Enchantrix/rfcs/RFC-0004-LTHN-Hash-Algorithm.md

406 lines
12 KiB
Markdown

# 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