# RFC-0004: LTHN Quasi-Salted Hash Algorithm

**Status:** Informational
**Version:** 1.0
**Created:** 2025-01-13
**Author:** Snider

## Abstract

This document specifies the LTHN (Leet-Hash-N) quasi-salted hash algorithm, a deterministic hashing scheme that derives a salt from the input itself using character substitution and reversal. LTHN produces reproducible hashes that can be verified without storing a separate salt value, making it suitable for checksums, identifiers, and non-security-critical hashing applications.

## Table of Contents

1. [Introduction](#1-introduction)
2. [Terminology](#2-terminology)
3. [Algorithm Specification](#3-algorithm-specification)
4. [Character Substitution Map](#4-character-substitution-map)
5. [Verification](#5-verification)
6. [Use Cases](#6-use-cases)
7. [Security Considerations](#7-security-considerations)
8. [Implementation Requirements](#8-implementation-requirements)
9. [Test Vectors](#9-test-vectors)
10. [References](#10-references)

## 1. Introduction

Traditional salted hashing requires storing a random salt value alongside the hash. This provides protection against rainbow table attacks but requires additional storage and management.

LTHN takes a different approach: the salt is derived deterministically from the input itself through a transformation that:

1. Reverses the input string
2. Applies character substitutions inspired by "leet speak" conventions

This produces a quasi-salt that varies with input content while remaining reproducible, enabling verification without salt storage.

### 1.1 Design Goals

- **Determinism**: Same input always produces same hash
- **Salt derivation**: No external salt storage required
- **Verifiability**: Hashes can be verified with only the input
- **Simplicity**: Easy to implement and understand
- **Interoperability**: Based on standard SHA-256

### 1.2 Non-Goals

LTHN is NOT designed to:
- Replace proper password hashing (use bcrypt, Argon2, etc.)
- Provide cryptographic security against determined attackers
- Resist preimage or collision attacks beyond SHA-256's guarantees

## 2. Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

**Input**: The original string to be hashed
**Quasi-salt**: A salt derived from the input itself
**Key map**: The character substitution table
**LTHN hash**: The final hash output

## 3. Algorithm Specification

### 3.1 Overview

```
LTHN(input) = SHA256(input || createSalt(input))
```

Where `||` denotes concatenation and `createSalt` is defined below.

### 3.2 Salt Creation Algorithm

```
function createSalt(input: string) -> string:
    if input is empty:
        return ""

    runes = input as array of Unicode code points
    salt = new array of size length(runes)

    for i = 0 to length(runes) - 1:
        // Reverse: take character from end
        char = runes[length(runes) - 1 - i]

        // Apply substitution if exists in key map
        if char in keyMap:
            salt[i] = keyMap[char]
        else:
            salt[i] = char

    return salt as string
```

### 3.3 Hash Algorithm

```
function Hash(input: string) -> string:
    salt = createSalt(input)
    combined = input + salt
    digest = SHA256(combined as UTF-8 bytes)
    return hexEncode(digest)
```

### 3.4 Output Format

- Output: 64-character lowercase hexadecimal string
- Digest: 32 bytes (256 bits)

## 4. Character Substitution Map

### 4.1 Default Key Map

The default substitution map uses bidirectional "leet speak" style mappings:

| Input | Output | Description |
|-------|--------|-------------|
| `o` | `0` | Letter O to zero |
| `l` | `1` | Letter L to one |
| `e` | `3` | Letter E to three |
| `a` | `4` | Letter A to four |
| `s` | `z` | Letter S to Z |
| `t` | `7` | Letter T to seven |
| `0` | `o` | Zero to letter O |
| `1` | `l` | One to letter L |
| `3` | `e` | Three to letter E |
| `4` | `a` | Four to letter A |
| `7` | `t` | Seven to letter T |

Note: The mapping is NOT fully symmetric. `z` does NOT map back to `s`.

### 4.2 Key Map as Code

```
keyMap = {
    'o': '0',
    'l': '1',
    'e': '3',
    'a': '4',
    's': 'z',
    't': '7',
    '0': 'o',
    '1': 'l',
    '3': 'e',
    '4': 'a',
    '7': 't'
}
```

### 4.3 Custom Key Maps

Implementations MAY support custom key maps. When using custom maps:

- Document the custom map clearly
- Ensure bidirectional mappings are intentional
- Consider character set implications (Unicode vs. ASCII)

## 5. Verification

### 5.1 Verification Algorithm

```
function Verify(input: string, expectedHash: string) -> bool:
    actualHash = Hash(input)
    return constantTimeCompare(actualHash, expectedHash)
```

### 5.2 Properties

- Verification requires only the input and hash
- No salt storage or retrieval necessary
- Same input always produces same hash

## 6. Use Cases

### 6.1 Recommended Uses

| Use Case | Suitability | Notes |
|----------|-------------|-------|
| Content identifiers | Good | Deterministic, reproducible |
| Cache keys | Good | Same content = same key |
| Deduplication | Good | Identify identical content |
| File integrity | Moderate | Use with checksum comparison |
| Non-critical checksums | Good | Simple verification |
| Rolling key derivation | Good | Time-based key rotation (see 6.3) |

### 6.2 Not Recommended Uses

| Use Case | Reason |
|----------|--------|
| Password storage | Use bcrypt, Argon2, or scrypt instead |
| Authentication tokens | Use HMAC or proper MACs |
| Digital signatures | Use proper signature schemes |
| Security-critical integrity | Use HMAC-SHA256 |

### 6.3 Rolling Key Derivation Pattern

LTHN is well-suited for deriving time-based rolling keys for streaming media or time-limited access control. The pattern combines a time period with user credentials:

```
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
```

#### 6.3.1 Cadence Formats

| Cadence | Period Format | Example | Window |
|---------|---------------|---------|--------|
| daily | YYYY-MM-DD | "2026-01-13" | 24 hours |
| 12h | YYYY-MM-DD-AM/PM | "2026-01-13-AM" | 12 hours |
| 6h | YYYY-MM-DD-HH | "2026-01-13-00" | 6 hours (00, 06, 12, 18) |
| 1h | YYYY-MM-DD-HH | "2026-01-13-15" | 1 hour |

#### 6.3.2 Rolling Window Implementation

For graceful key transitions, implementations should support a rolling window:

```
function GetRollingPeriods(cadence: string) -> (current: string, next: string):
    now = currentTime()
    current = formatPeriod(now, cadence)
    next = formatPeriod(now + periodDuration(cadence), cadence)
    return (current, next)
```

Content encrypted with rolling keys includes wrapped CEKs (Content Encryption Keys) for both current and next periods, allowing decryption during period transitions.

#### 6.3.3 CEK Wrapping

```
// Wrap CEK for distribution
For each period in [current, next]:
    streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
    wrappedCEK = ChaCha20Poly1305_Encrypt(CEK, streamKey)
    store (period, wrappedCEK) in header

// Unwrap CEK for playback
For each (period, wrappedCEK) in header:
    streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
    CEK = ChaCha20Poly1305_Decrypt(wrappedCEK, streamKey)
    if success: return CEK
return error("no valid key for current period")
```

## 7. Security Considerations

### 7.1 Not a Password Hash

LTHN MUST NOT be used for password hashing because:

- No work factor (bcrypt, Argon2 have tunable cost)
- No random salt (predictable salt derivation)
- Fast to compute (enables brute force)
- No memory hardness (GPU/ASIC friendly)

### 7.2 Quasi-Salt Limitations

The derived salt provides limited protection:

- Salt is deterministic, not random
- Identical inputs produce identical salts
- Does not prevent rainbow tables for known inputs
- Salt derivation algorithm is public

### 7.3 SHA-256 Dependency

Security properties depend on SHA-256:

- Preimage resistance: Finding input from hash is hard
- Second preimage resistance: Finding different input with same hash is hard
- Collision resistance: Finding two inputs with same hash is hard

These properties apply to the combined `input || salt` value.

### 7.4 Timing Attacks

Verification SHOULD use constant-time comparison to prevent timing attacks:

```
function constantTimeCompare(a: string, b: string) -> bool:
    if length(a) != length(b):
        return false

    result = 0
    for i = 0 to length(a) - 1:
        result |= a[i] XOR b[i]

    return result == 0
```

## 8. Implementation Requirements

Conforming implementations MUST:

1. Use SHA-256 as the underlying hash function
2. Concatenate input and salt in the order: `input || salt`
3. Use the default key map unless explicitly configured otherwise
4. Output lowercase hexadecimal encoding
5. Handle empty strings by returning SHA-256 of empty string
6. Support Unicode input (process as UTF-8 bytes after salt creation)

Conforming implementations SHOULD:

1. Provide constant-time verification
2. Support custom key maps via configuration
3. Document any deviations from the default key map

## 9. Test Vectors

### 9.1 Basic Test Cases

| Input | Salt | Combined | LTHN Hash |
|-------|------|----------|-----------|
| `""` | `""` | `""` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |
| `"a"` | `"4"` | `"a4"` | `a4a4e5c4b3b2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6` |
| `"hello"` | `"011eh"` | `"hello011eh"` | (computed) |
| `"test"` | `"7z37"` | `"test7z37"` | (computed) |

### 9.2 Character Substitution Examples

| Input | Reversed | After Substitution (Salt) |
|-------|----------|---------------------------|
| `"hello"` | `"olleh"` | `"011eh"` |
| `"test"` | `"tset"` | `"7z37"` |
| `"password"` | `"drowssap"` | `"dr0wzz4p"` |
| `"12345"` | `"54321"` | `"5ae2l"` |

### 9.3 Unicode Test Cases

| Input | Expected Behavior |
|-------|-------------------|
| `"cafe"` | Standard processing |
| `"caf`e`"` | e with accent NOT substituted (only ASCII 'e' matches) |

Note: Key map only matches exact character codes, not normalized equivalents.

## 10. API Reference

### 10.1 Go API

```go
import "github.com/Snider/Enchantrix/pkg/crypt"

// Create crypt service
svc := crypt.NewService()

// Hash with LTHN
hash := svc.Hash(crypt.LTHN, "input string")

// Available hash types
crypt.LTHN      // LTHN quasi-salted hash
crypt.SHA256    // Standard SHA-256
crypt.SHA512    // Standard SHA-512
// ... other standard algorithms
```

### 10.2 Direct Usage

```go
import "github.com/Snider/Enchantrix/pkg/crypt/std/lthn"

// Direct LTHN hash
hash := lthn.Hash("input string")

// Verify hash
valid := lthn.Verify("input string", expectedHash)
```

## 11. Future Work

- [ ] Custom key map configuration via API
- [ ] WASM compilation for browser-based LTHN operations
- [ ] Alternative underlying hash functions (SHA-3, BLAKE3)
- [ ] Configurable salt derivation strategies
- [ ] Performance optimization for high-throughput scenarios
- [ ] Formal security analysis of rolling key pattern

## 12. References

- [FIPS 180-4] Secure Hash Standard (SHA-256)
- [RFC 4648] The Base16, Base32, and Base64 Data Encodings
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
- [Wikipedia: Leet] History and conventions of leet speak character substitution

---

## Appendix A: Reference Implementation

A reference implementation in Go is available at:
`github.com/Snider/Enchantrix/pkg/crypt/std/lthn/lthn.go`

## Appendix B: Historical Note

The name "LTHN" derives from "Leet Hash N" or "Lethean" (relating to forgetfulness/oblivion in Greek mythology), referencing both the leet-speak character substitutions and the one-way nature of hash functions.

## Appendix C: Comparison with Other Schemes

| Scheme | Salt | Work Factor | Suitable for Passwords |
|--------|------|-------------|------------------------|
| LTHN | Derived | None | No |
| SHA-256 | None | None | No |
| HMAC-SHA256 | Key-based | None | No |
| bcrypt | Random | Yes | Yes |
| Argon2 | Random | Yes | Yes |
| scrypt | Random | Yes | Yes |

## Appendix D: Changelog

- **1.0** (2025-01-13): Initial specification
