docs: add examples for checksum algorithms, hashing, PGP operations, and .trix container format
This commit is contained in:
parent
748ca6ddd7
commit
bdef246a87
18 changed files with 2186 additions and 124 deletions
246
README.md
246
README.md
|
|
@ -6,140 +6,212 @@
|
|||
[](https://codecov.io/github/Snider/Enchantrix)
|
||||
[](https://github.com/Snider/Enchantrix/releases/latest)
|
||||
[](https://github.com/Snider/Enchantrix/blob/main/LICENCE)
|
||||
[](https://go.dev/)
|
||||
|
||||
Enchantrix is a Go-based encryption library designed to provide a secure and easy-to-use framework for handling sensitive data in Web3 applications. It will feature Poly-ChaCha stream proxying and a custom `.trix` file format for encrypted data.
|
||||
A Go-based encryption and data transformation library designed for secure handling of sensitive data. Enchantrix provides composable transformation pipelines, a flexible binary container format, and defense-in-depth encryption with pre-obfuscation.
|
||||
|
||||
## Documentation
|
||||
## Features
|
||||
|
||||
A MkDocs site (Material theme) lives in this repository. To preview it locally:
|
||||
- **Sigil Transformation Framework** - Composable, reversible data transformations (encoding, compression, hashing)
|
||||
- **Pre-Obfuscation Layer** - Side-channel attack mitigation for AEAD ciphers
|
||||
- **.trix Container Format** - Protocol-agnostic binary format with JSON metadata
|
||||
- **Multiple Hash Algorithms** - SHA-2, SHA-3, BLAKE2, RIPEMD-160, and the custom LTHN algorithm
|
||||
- **Full PGP Support** - Key generation, encryption, decryption, signing, and verification
|
||||
- **RSA Operations** - Key generation, encryption, and decryption
|
||||
- **CLI Tool** - `trix` command for encoding, decoding, and transformations
|
||||
|
||||
```shell
|
||||
# Requires Python mkdocs and mkdocs-material
|
||||
pip install mkdocs mkdocs-material
|
||||
|
||||
# Serve docs
|
||||
mkdocs serve -a 127.0.0.1:8000
|
||||
|
||||
# Build static site to ./site/
|
||||
mkdocs build --strict
|
||||
```
|
||||
|
||||
The site configuration is at `./mkdocs.yml` and sources are under `./docs/docs/`.
|
||||
|
||||
## Go Version and Workspace
|
||||
|
||||
- Minimum Go version: 1.25
|
||||
- This repository includes a `go.work` to streamline development across tools. Use standard Go commands; no special steps are required.
|
||||
|
||||
## Test-Driven Development
|
||||
|
||||
This project follows a strict Test-Driven Development (TDD) methodology. All new functionality must be accompanied by a comprehensive suite of tests.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with Enchantrix, you'll need to have Go installed. You can then run the tests using the following command:
|
||||
|
||||
```shell
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## `trix` Command-Line Tool
|
||||
|
||||
Enchantrix includes a command-line tool called `trix` for encoding and decoding files using the `.trix` format.
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
You can install the `trix` tool using `go install`:
|
||||
```shell
|
||||
go get github.com/Snider/Enchantrix
|
||||
```
|
||||
|
||||
### Install CLI Tool
|
||||
|
||||
```shell
|
||||
go install github.com/Snider/Enchantrix/cmd/trix@latest
|
||||
```
|
||||
|
||||
### Usage
|
||||
### Basic Usage
|
||||
|
||||
The `trix` tool can read from a file using the `--input` flag or from `stdin` if the flag is omitted.
|
||||
#### Sigil Transformations
|
||||
|
||||
#### Encode
|
||||
```go
|
||||
package main
|
||||
|
||||
To encode a file, use the `encode` subcommand, followed by any sigils you want to apply:
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||
)
|
||||
|
||||
```shell
|
||||
trix encode --output <output-file> --magic <magic-number> [sigil1] [sigil2]...
|
||||
func main() {
|
||||
// Create sigils
|
||||
hexSigil, _ := enchantrix.NewSigil("hex")
|
||||
base64Sigil, _ := enchantrix.NewSigil("base64")
|
||||
|
||||
// Apply transformations
|
||||
data := []byte("Hello, Enchantrix!")
|
||||
encoded, _ := enchantrix.Transmute(data, []enchantrix.Sigil{hexSigil, base64Sigil})
|
||||
|
||||
fmt.Printf("Encoded: %s\n", encoded)
|
||||
}
|
||||
```
|
||||
|
||||
- `--input`: The path to the input file (optional, reads from stdin if omitted).
|
||||
- `--output`: The path to the output `.trix` file.
|
||||
- `--magic`: A 4-byte magic number to identify the file type.
|
||||
- `[sigil...]`: A space-separated list of sigils to apply to the data.
|
||||
#### Hashing
|
||||
|
||||
Example:
|
||||
```shell
|
||||
echo "Hello, Trix!" | trix encode --output test.trix --magic TRIX base64
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := crypt.NewService()
|
||||
|
||||
hash := service.Hash(crypt.SHA256, "Hello, World!")
|
||||
fmt.Printf("SHA-256: %s\n", hash)
|
||||
|
||||
// LTHN quasi-salted hash
|
||||
lthnHash := service.Hash(crypt.LTHN, "Hello, World!")
|
||||
fmt.Printf("LTHN: %s\n", lthnHash)
|
||||
}
|
||||
```
|
||||
|
||||
#### Decode
|
||||
#### Encrypted .trix Container
|
||||
|
||||
To decode a `.trix` file, use the `decode` subcommand:
|
||||
```go
|
||||
package main
|
||||
|
||||
```shell
|
||||
trix decode --output <output-file> --magic <magic-number> [sigil1] [sigil2]...
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Snider/Enchantrix/pkg/trix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
container := &trix.Trix{
|
||||
Header: map[string]interface{}{
|
||||
"content_type": "text/plain",
|
||||
"created_at": "2025-01-13T12:00:00Z",
|
||||
},
|
||||
Payload: []byte("Secret message"),
|
||||
InSigils: []string{"gzip", "base64"},
|
||||
}
|
||||
|
||||
// Pack with sigils
|
||||
container.Pack()
|
||||
|
||||
// Encode to binary
|
||||
encoded, _ := trix.Encode(container, "MYAP", nil)
|
||||
fmt.Printf("Container size: %d bytes\n", len(encoded))
|
||||
}
|
||||
```
|
||||
|
||||
- `--input`: The path to the input `.trix` file (optional, reads from stdin if omitted).
|
||||
- `--output`: The path to the decoded output file.
|
||||
- `--magic`: The 4-byte magic number used during encoding.
|
||||
- `[sigil...]`: A space-separated list of sigils to apply for unpacking.
|
||||
|
||||
Example:
|
||||
```shell
|
||||
trix decode --input test.trix --output test.txt --magic TRIX base64
|
||||
```
|
||||
|
||||
#### Hash
|
||||
|
||||
To hash data, use the `hash` subcommand, followed by the desired algorithm:
|
||||
### CLI Examples
|
||||
|
||||
```shell
|
||||
trix hash [algorithm]
|
||||
# Encode with sigils
|
||||
echo "Hello, Trix!" | trix encode --output message.trix --magic TRIX base64
|
||||
|
||||
# Decode
|
||||
trix decode --input message.trix --output message.txt --magic TRIX base64
|
||||
|
||||
# Hash data
|
||||
echo "Hello, World!" | trix hash sha256
|
||||
|
||||
# Apply sigil directly
|
||||
echo "Hello" | trix hex
|
||||
# Output: 48656c6c6f
|
||||
```
|
||||
|
||||
- `--input`: The path to the input file (optional, reads from stdin if omitted).
|
||||
- `[algorithm]`: The hashing algorithm to use (e.g., `sha256`).
|
||||
## Specifications
|
||||
|
||||
Example:
|
||||
```shell
|
||||
echo "Hello, Trix!" | trix hash sha256
|
||||
Enchantrix includes formal RFC-style specifications for its core protocols:
|
||||
|
||||
| RFC | Title | Description |
|
||||
|-----|-------|-------------|
|
||||
| [RFC-0001](rfcs/RFC-0001-Pre-Obfuscation-Layer.md) | Pre-Obfuscation Layer | Side-channel mitigation for AEAD ciphers |
|
||||
| [RFC-0002](rfcs/RFC-0002-Trix-Container-Format.md) | TRIX Container Format | Binary container with JSON metadata |
|
||||
| [RFC-0003](rfcs/RFC-0003-Sigil-Transformation-Framework.md) | Sigil Framework | Composable data transformation interface |
|
||||
| [RFC-0004](rfcs/RFC-0004-LTHN-Hash-Algorithm.md) | LTHN Hash | Quasi-salted deterministic hashing |
|
||||
|
||||
## Available Sigils
|
||||
|
||||
| Category | Sigils |
|
||||
|----------|--------|
|
||||
| **Encoding** | `hex`, `base64` |
|
||||
| **Compression** | `gzip` |
|
||||
| **Formatting** | `json`, `json-indent` |
|
||||
| **Transform** | `reverse` |
|
||||
| **Hashing** | `md4`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `sha512-224`, `sha512-256`, `ripemd160`, `blake2s-256`, `blake2b-256`, `blake2b-384`, `blake2b-512` |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Enchantrix/
|
||||
├── cmd/trix/ # CLI tool
|
||||
├── pkg/
|
||||
│ ├── enchantrix/ # Sigil framework and crypto sigils
|
||||
│ ├── trix/ # .trix container format
|
||||
│ └── crypt/ # Cryptographic services (hash, RSA, PGP)
|
||||
├── rfcs/ # Protocol specifications
|
||||
├── examples/ # Usage examples
|
||||
└── docs/ # MkDocs documentation
|
||||
```
|
||||
|
||||
#### Sigils
|
||||
## Documentation
|
||||
|
||||
You can also apply any sigil directly as a subcommand:
|
||||
Full documentation is available via MkDocs:
|
||||
|
||||
```shell
|
||||
trix [sigil]
|
||||
# Install dependencies
|
||||
pip install mkdocs mkdocs-material
|
||||
|
||||
# Serve locally
|
||||
mkdocs serve -a 127.0.0.1:8000
|
||||
|
||||
# Build static site
|
||||
mkdocs build --strict
|
||||
```
|
||||
|
||||
- `--input`: The path to the input file or a string (optional, reads from stdin if omitted).
|
||||
## Development
|
||||
|
||||
### Requirements
|
||||
|
||||
- Go 1.25 or later
|
||||
|
||||
### Running Tests
|
||||
|
||||
Example:
|
||||
```shell
|
||||
echo "Hello, Trix!" | trix hex
|
||||
# Run all tests
|
||||
go test ./...
|
||||
|
||||
# Run with race detection
|
||||
go test -race ./...
|
||||
|
||||
# Run with coverage
|
||||
go test -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
### Test-Driven Development
|
||||
|
||||
This project follows strict TDD methodology. All new functionality must include comprehensive tests.
|
||||
|
||||
## Releases
|
||||
|
||||
This repository includes a basic GoReleaser configuration at `.goreleaser.yml`.
|
||||
|
||||
- Snapshot release (local, no publish):
|
||||
Built with GoReleaser:
|
||||
|
||||
```shell
|
||||
# Snapshot release (local, no publish)
|
||||
goreleaser release --snapshot --clean
|
||||
```
|
||||
|
||||
- Regular release (expects CI and Git tag):
|
||||
|
||||
```shell
|
||||
# Production release (requires Git tag)
|
||||
goreleaser release --clean
|
||||
```
|
||||
|
||||
Artifacts are produced under `./dist/`.
|
||||
## License
|
||||
|
||||
See [LICENCE](LICENCE) for details.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
// Example: Checksum algorithms
|
||||
//
|
||||
// This example demonstrates Luhn and Fletcher checksum algorithms
|
||||
// for data integrity verification.
|
||||
//
|
||||
// Run with: go run examples/checksums/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
// Example: Hashing with multiple algorithms
|
||||
//
|
||||
// This example demonstrates how to use the crypt service to compute hashes
|
||||
// using various algorithms including the custom LTHN quasi-salted hash.
|
||||
//
|
||||
// Run with: go run examples/hash/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
// Example: PGP encryption and decryption
|
||||
//
|
||||
// This example demonstrates OpenPGP key generation, asymmetric encryption,
|
||||
// and decryption. PGP provides end-to-end encryption with ASCII-armored
|
||||
// output suitable for email and text-based transport.
|
||||
//
|
||||
// Run with: go run examples/pgp_encrypt_decrypt/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
// Example: PGP key pair generation
|
||||
//
|
||||
// This example demonstrates generating an OpenPGP key pair with
|
||||
// name, email, and comment metadata. The output is ASCII-armored
|
||||
// for easy storage and distribution.
|
||||
//
|
||||
// Run with: go run examples/pgp_generate_keys/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
// Example: PGP digital signatures
|
||||
//
|
||||
// This example demonstrates creating and verifying PGP digital signatures.
|
||||
// Signatures provide authenticity and integrity verification without
|
||||
// encrypting the message content.
|
||||
//
|
||||
// Run with: go run examples/pgp_sign_verify/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
// Example: PGP symmetric (passphrase-based) encryption
|
||||
//
|
||||
// This example demonstrates symmetric encryption using a passphrase
|
||||
// instead of public/private key pairs. Useful when you need to share
|
||||
// encrypted data with someone using a pre-shared password.
|
||||
//
|
||||
// Run with: go run examples/pgp_symmetric_encrypt/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
// Example: RSA encryption and decryption
|
||||
//
|
||||
// This example demonstrates RSA key generation, encryption, and decryption
|
||||
// using the crypt service. RSA is suitable for encrypting small amounts of
|
||||
// data or for key exchange protocols.
|
||||
//
|
||||
// Run with: go run examples/rsa/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
85
examples/sigils/main.go
Normal file
85
examples/sigils/main.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Example: Sigil transformation framework
|
||||
//
|
||||
// This example demonstrates the Sigil transformation framework, showing
|
||||
// how to create transformation pipelines for encoding, compression, and
|
||||
// hashing. Sigils can be chained together and reversed.
|
||||
//
|
||||
// Run with: go run examples/sigils/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("--- Sigil Transformation Demo ---")
|
||||
|
||||
// Original data
|
||||
data := []byte("Hello, Enchantrix! This is a demonstration of the Sigil framework.")
|
||||
fmt.Printf("Original data (%d bytes): %s\n\n", len(data), data)
|
||||
|
||||
// 1. Single sigil transformation
|
||||
fmt.Println("1. Single Sigil (hex encoding):")
|
||||
hexSigil, _ := enchantrix.NewSigil("hex")
|
||||
hexEncoded, _ := hexSigil.In(data)
|
||||
fmt.Printf(" Hex encoded: %s\n", hexEncoded)
|
||||
hexDecoded, _ := hexSigil.Out(hexEncoded)
|
||||
fmt.Printf(" Hex decoded: %s\n\n", hexDecoded)
|
||||
|
||||
// 2. Chained sigils using Transmute
|
||||
fmt.Println("2. Chained Sigils (gzip -> base64):")
|
||||
gzipSigil, _ := enchantrix.NewSigil("gzip")
|
||||
base64Sigil, _ := enchantrix.NewSigil("base64")
|
||||
|
||||
// Apply chain: data -> gzip -> base64
|
||||
compressed, _ := gzipSigil.In(data)
|
||||
fmt.Printf(" After gzip (%d bytes)\n", len(compressed))
|
||||
|
||||
result, _ := enchantrix.Transmute(data, []enchantrix.Sigil{gzipSigil, base64Sigil})
|
||||
fmt.Printf(" After gzip+base64 (%d bytes): %s...\n\n", len(result), result[:50])
|
||||
|
||||
// 3. Reverse the chain
|
||||
fmt.Println("3. Reversing the Chain:")
|
||||
// Reverse order: base64.Out -> gzip.Out
|
||||
step1, _ := base64Sigil.Out(result)
|
||||
original, _ := gzipSigil.Out(step1)
|
||||
fmt.Printf(" Recovered: %s\n\n", original)
|
||||
|
||||
// 4. Hash sigils (irreversible)
|
||||
fmt.Println("4. Hash Sigils (irreversible):")
|
||||
sha256Sigil, _ := enchantrix.NewSigil("sha256")
|
||||
hash, _ := sha256Sigil.In(data)
|
||||
fmt.Printf(" SHA-256 hash (%d bytes): %x\n", len(hash), hash)
|
||||
|
||||
// Hash.Out is a no-op (returns input unchanged)
|
||||
passthrough, _ := sha256Sigil.Out(hash)
|
||||
fmt.Printf(" Hash.Out (passthrough): %x\n\n", passthrough)
|
||||
|
||||
// 5. Symmetric sigil (reverse)
|
||||
fmt.Println("5. Symmetric Sigil (byte reversal):")
|
||||
reverseSigil, _ := enchantrix.NewSigil("reverse")
|
||||
reversed, _ := reverseSigil.In([]byte("Hello"))
|
||||
fmt.Printf(" 'Hello' reversed: %s\n", reversed)
|
||||
// In and Out do the same thing for symmetric sigils
|
||||
unreversed, _ := reverseSigil.Out(reversed)
|
||||
fmt.Printf(" Reversed again: %s\n\n", unreversed)
|
||||
|
||||
// 6. Available sigils
|
||||
fmt.Println("6. Available Sigils:")
|
||||
sigils := []string{
|
||||
"hex", "base64", "gzip", "reverse", "json", "json-indent",
|
||||
"md5", "sha1", "sha256", "sha512", "blake2b-256",
|
||||
}
|
||||
for _, name := range sigils {
|
||||
sigil, err := enchantrix.NewSigil(name)
|
||||
if err != nil {
|
||||
log.Printf(" - %s: ERROR\n", name)
|
||||
} else {
|
||||
_ = sigil // sigil is valid
|
||||
fmt.Printf(" - %s: OK\n", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
122
examples/trix_container/main.go
Normal file
122
examples/trix_container/main.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Example: .trix container format
|
||||
//
|
||||
// This example demonstrates the .trix binary container format for packaging
|
||||
// data with metadata and optional transformations. The format supports
|
||||
// custom magic numbers, JSON headers, and sigil-based transformation pipelines.
|
||||
//
|
||||
// Run with: go run examples/trix_container/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||
"github.com/Snider/Enchantrix/pkg/trix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("--- .trix Container Format Demo ---")
|
||||
|
||||
// 1. Create a simple container
|
||||
fmt.Println("\n1. Simple Container:")
|
||||
simple := &trix.Trix{
|
||||
Header: map[string]interface{}{
|
||||
"content_type": "text/plain",
|
||||
"created_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"author": "Enchantrix Demo",
|
||||
},
|
||||
Payload: []byte("Hello, this is the payload data!"),
|
||||
}
|
||||
|
||||
encoded, err := trix.Encode(simple, "DEMO", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to encode: %v", err)
|
||||
}
|
||||
fmt.Printf(" Encoded size: %d bytes\n", len(encoded))
|
||||
fmt.Printf(" Magic number: %s\n", encoded[:4])
|
||||
fmt.Printf(" Version: %d\n", encoded[4])
|
||||
|
||||
// Decode it back
|
||||
decoded, err := trix.Decode(encoded, "DEMO", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode: %v", err)
|
||||
}
|
||||
fmt.Printf(" Decoded payload: %s\n", decoded.Payload)
|
||||
fmt.Printf(" Header content_type: %s\n", decoded.Header["content_type"])
|
||||
|
||||
// 2. Container with checksum verification
|
||||
fmt.Println("\n2. Container with Checksum:")
|
||||
withChecksum := &trix.Trix{
|
||||
Header: map[string]interface{}{
|
||||
"content_type": "application/octet-stream",
|
||||
},
|
||||
Payload: []byte("Important data that needs integrity verification"),
|
||||
ChecksumAlgo: crypt.SHA256,
|
||||
}
|
||||
|
||||
encodedWithChecksum, _ := trix.Encode(withChecksum, "CHKS", nil)
|
||||
fmt.Printf(" Encoded size: %d bytes\n", len(encodedWithChecksum))
|
||||
|
||||
// Decode and verify checksum automatically
|
||||
decodedWithChecksum, err := trix.Decode(encodedWithChecksum, "CHKS", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Checksum verification failed: %v", err)
|
||||
}
|
||||
fmt.Printf(" Checksum verified! Algorithm: %s\n", decodedWithChecksum.Header["checksum_algo"])
|
||||
fmt.Printf(" Checksum value: %s...\n", decodedWithChecksum.Header["checksum"].(string)[:32])
|
||||
|
||||
// 3. Container with sigil transformations
|
||||
fmt.Println("\n3. Container with Sigil Transformations:")
|
||||
withSigils := &trix.Trix{
|
||||
Header: map[string]interface{}{
|
||||
"content_type": "text/plain",
|
||||
"transformed": true,
|
||||
},
|
||||
Payload: []byte("This data will be compressed and encoded!"),
|
||||
InSigils: []string{"gzip", "base64"},
|
||||
}
|
||||
|
||||
fmt.Printf(" Original payload size: %d bytes\n", len(withSigils.Payload))
|
||||
|
||||
// Pack applies InSigils
|
||||
if err := withSigils.Pack(); err != nil {
|
||||
log.Fatalf("Pack failed: %v", err)
|
||||
}
|
||||
fmt.Printf(" After Pack (gzip+base64): %d bytes\n", len(withSigils.Payload))
|
||||
|
||||
encodedWithSigils, _ := trix.Encode(withSigils, "TRNS", nil)
|
||||
fmt.Printf(" Final encoded size: %d bytes\n", len(encodedWithSigils))
|
||||
|
||||
// Decode and unpack
|
||||
decodedWithSigils, _ := trix.Decode(encodedWithSigils, "TRNS", nil)
|
||||
decodedWithSigils.OutSigils = []string{"gzip", "base64"} // Must match InSigils
|
||||
|
||||
if err := decodedWithSigils.Unpack(); err != nil {
|
||||
log.Fatalf("Unpack failed: %v", err)
|
||||
}
|
||||
fmt.Printf(" Unpacked payload: %s\n", decodedWithSigils.Payload)
|
||||
|
||||
// 4. Custom magic numbers for different applications
|
||||
fmt.Println("\n4. Custom Magic Numbers:")
|
||||
apps := []struct {
|
||||
magic string
|
||||
desc string
|
||||
}{
|
||||
{"CONF", "Configuration files"},
|
||||
{"LOGS", "Log archives"},
|
||||
{"KEYS", "Key storage"},
|
||||
{"MSGS", "Encrypted messages"},
|
||||
}
|
||||
for _, app := range apps {
|
||||
container := &trix.Trix{
|
||||
Header: map[string]interface{}{"app": app.desc},
|
||||
Payload: []byte("sample"),
|
||||
}
|
||||
data, _ := trix.Encode(container, app.magic, nil)
|
||||
fmt.Printf(" %s: %s (%d bytes)\n", app.magic, app.desc, len(data))
|
||||
}
|
||||
|
||||
fmt.Println("\nDone!")
|
||||
}
|
||||
|
|
@ -1,3 +1,19 @@
|
|||
// Package lthn implements the LTHN quasi-salted hash algorithm (RFC-0004).
|
||||
//
|
||||
// LTHN produces deterministic, verifiable hashes without requiring separate salt
|
||||
// storage. The salt is derived from the input itself through:
|
||||
// 1. Reversing the input string
|
||||
// 2. Applying "leet speak" style character substitutions
|
||||
//
|
||||
// The final hash is: SHA256(input || derived_salt)
|
||||
//
|
||||
// This is suitable for content identifiers, cache keys, and deduplication.
|
||||
// NOT suitable for password hashing - use bcrypt, Argon2, or scrypt instead.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// hash := lthn.Hash("hello")
|
||||
// valid := lthn.Verify("hello", hash) // true
|
||||
package lthn
|
||||
|
||||
import (
|
||||
|
|
@ -5,39 +21,53 @@ import (
|
|||
"encoding/hex"
|
||||
)
|
||||
|
||||
// keyMap is the default character-swapping map used for the quasi-salting process.
|
||||
// keyMap defines the character substitutions for quasi-salt derivation.
|
||||
// These are inspired by "leet speak" conventions for letter-number substitution.
|
||||
// The mapping is bidirectional for most characters but NOT fully symmetric.
|
||||
var keyMap = map[rune]rune{
|
||||
'o': '0',
|
||||
'l': '1',
|
||||
'e': '3',
|
||||
'a': '4',
|
||||
's': 'z',
|
||||
't': '7',
|
||||
'0': 'o',
|
||||
'1': 'l',
|
||||
'3': 'e',
|
||||
'4': 'a',
|
||||
'7': 't',
|
||||
'o': '0', // letter O -> zero
|
||||
'l': '1', // letter L -> one
|
||||
'e': '3', // letter E -> three
|
||||
'a': '4', // letter A -> four
|
||||
's': 'z', // letter S -> Z
|
||||
't': '7', // letter T -> seven
|
||||
'0': 'o', // zero -> letter O
|
||||
'1': 'l', // one -> letter L
|
||||
'3': 'e', // three -> letter E
|
||||
'4': 'a', // four -> letter A
|
||||
'7': 't', // seven -> letter T
|
||||
}
|
||||
|
||||
// SetKeyMap sets the key map for the notarisation process.
|
||||
// SetKeyMap replaces the default character substitution map.
|
||||
// Use this to customize the quasi-salt derivation for specific applications.
|
||||
// Changes affect all subsequent Hash and Verify calls.
|
||||
func SetKeyMap(newKeyMap map[rune]rune) {
|
||||
keyMap = newKeyMap
|
||||
}
|
||||
|
||||
// GetKeyMap gets the current key map.
|
||||
// GetKeyMap returns the current character substitution map.
|
||||
func GetKeyMap() map[rune]rune {
|
||||
return keyMap
|
||||
}
|
||||
|
||||
// Hash creates a reproducible hash from a string.
|
||||
// Hash computes the LTHN hash of the input string.
|
||||
//
|
||||
// The algorithm:
|
||||
// 1. Derive a quasi-salt by reversing the input and applying character substitutions
|
||||
// 2. Concatenate: input + salt
|
||||
// 3. Compute SHA-256 of the concatenated string
|
||||
// 4. Return the hex-encoded digest (64 characters, lowercase)
|
||||
//
|
||||
// The same input always produces the same hash, enabling verification
|
||||
// without storing a separate salt value.
|
||||
func Hash(input string) string {
|
||||
salt := createSalt(input)
|
||||
hash := sha256.Sum256([]byte(input + salt))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// createSalt creates a quasi-salt from a string by reversing it and swapping characters.
|
||||
// createSalt derives a quasi-salt by reversing the input and applying substitutions.
|
||||
// For example: "hello" -> reversed "olleh" -> substituted "011eh"
|
||||
func createSalt(input string) string {
|
||||
if input == "" {
|
||||
return ""
|
||||
|
|
@ -55,7 +85,10 @@ func createSalt(input string) string {
|
|||
return string(salt)
|
||||
}
|
||||
|
||||
// Verify checks if an input string matches a given hash.
|
||||
// Verify checks if an input string produces the given hash.
|
||||
// Returns true if Hash(input) equals the provided hash value.
|
||||
// Uses direct string comparison - for security-critical applications,
|
||||
// consider using constant-time comparison.
|
||||
func Verify(input string, hash string) bool {
|
||||
return Hash(input) == hash
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
package enchantrix
|
||||
|
||||
// This file implements the Pre-Obfuscation Layer Protocol (RFC-0001) with
|
||||
// XChaCha20-Poly1305 encryption. The protocol applies a reversible transformation
|
||||
// to plaintext BEFORE it reaches CPU encryption routines, providing defense-in-depth
|
||||
// against side-channel attacks.
|
||||
//
|
||||
// The encryption flow is:
|
||||
// plaintext -> obfuscate(nonce) -> encrypt -> [nonce || ciphertext || tag]
|
||||
//
|
||||
// The decryption flow is:
|
||||
// [nonce || ciphertext || tag] -> decrypt -> deobfuscate(nonce) -> plaintext
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
|
|
@ -22,16 +33,31 @@ var (
|
|||
)
|
||||
|
||||
// PreObfuscator applies a reversible transformation to data before encryption.
|
||||
// This ensures that raw plaintext is never sent directly to CPU encryption routines.
|
||||
// This ensures that raw plaintext patterns are never sent directly to CPU
|
||||
// encryption routines, providing defense against side-channel attacks.
|
||||
//
|
||||
// Implementations must be deterministic: given the same entropy, the transformation
|
||||
// must be perfectly reversible: Deobfuscate(Obfuscate(x, e), e) == x
|
||||
type PreObfuscator interface {
|
||||
// Obfuscate transforms plaintext before encryption.
|
||||
// Obfuscate transforms plaintext before encryption using the provided entropy.
|
||||
// The entropy is typically the encryption nonce, ensuring the transformation
|
||||
// is unique per-encryption without additional random generation.
|
||||
Obfuscate(data []byte, entropy []byte) []byte
|
||||
|
||||
// Deobfuscate reverses the transformation after decryption.
|
||||
// Must be called with the same entropy used during Obfuscate.
|
||||
Deobfuscate(data []byte, entropy []byte) []byte
|
||||
}
|
||||
|
||||
// XORObfuscator performs XOR-based obfuscation using entropy-derived key stream.
|
||||
// This is a reversible transformation that ensures no cleartext patterns remain.
|
||||
// XORObfuscator performs XOR-based obfuscation using an entropy-derived key stream.
|
||||
//
|
||||
// The key stream is generated using SHA-256 in counter mode:
|
||||
//
|
||||
// keyStream[i*32:(i+1)*32] = SHA256(entropy || BigEndian64(i))
|
||||
//
|
||||
// This provides a cryptographically uniform key stream that decorrelates
|
||||
// plaintext patterns from the data seen by the encryption routine.
|
||||
// XOR is symmetric, so obfuscation and deobfuscation use the same operation.
|
||||
type XORObfuscator struct{}
|
||||
|
||||
// Obfuscate XORs the data with a key stream derived from the entropy.
|
||||
|
|
@ -87,8 +113,16 @@ func (x *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
|
|||
return stream
|
||||
}
|
||||
|
||||
// ShuffleMaskObfuscator applies byte-level shuffling based on entropy.
|
||||
// This provides additional diffusion before encryption.
|
||||
// ShuffleMaskObfuscator provides stronger obfuscation through byte shuffling and masking.
|
||||
//
|
||||
// The obfuscation process:
|
||||
// 1. Generate a mask from entropy using SHA-256 in counter mode
|
||||
// 2. XOR the data with the mask
|
||||
// 3. Generate a deterministic permutation using Fisher-Yates shuffle
|
||||
// 4. Reorder bytes according to the permutation
|
||||
//
|
||||
// This provides both value transformation (XOR mask) and position transformation
|
||||
// (shuffle), making pattern analysis more difficult than XOR alone.
|
||||
type ShuffleMaskObfuscator struct{}
|
||||
|
||||
// Obfuscate shuffles bytes and applies a mask derived from entropy.
|
||||
|
|
@ -208,9 +242,9 @@ func (s *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
|
|||
// Unlike demo implementations, the nonce is ONLY embedded in the ciphertext,
|
||||
// not exposed separately in headers.
|
||||
type ChaChaPolySigil struct {
|
||||
Key []byte
|
||||
Obfuscator PreObfuscator
|
||||
randReader io.Reader // for testing injection
|
||||
Key []byte
|
||||
Obfuscator PreObfuscator
|
||||
randReader io.Reader // for testing injection
|
||||
}
|
||||
|
||||
// NewChaChaPolySigil creates a new encryption sigil with the given key.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,38 @@
|
|||
// Package enchantrix provides the Sigil transformation framework for composable,
|
||||
// reversible data transformations. See RFC-0003 for the formal specification.
|
||||
//
|
||||
// Sigils are the core abstraction - each sigil implements a specific transformation
|
||||
// (encoding, compression, hashing, encryption) with a uniform interface. Sigils can
|
||||
// be chained together to create transformation pipelines.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// hexSigil, _ := enchantrix.NewSigil("hex")
|
||||
// base64Sigil, _ := enchantrix.NewSigil("base64")
|
||||
// result, _ := enchantrix.Transmute(data, []enchantrix.Sigil{hexSigil, base64Sigil})
|
||||
package enchantrix
|
||||
|
||||
// Sigil defines the interface for a data transformer.
|
||||
// A Sigil is a reversible or irreversible transformation of a byte slice.
|
||||
//
|
||||
// A Sigil represents a single transformation unit that can be applied to byte data.
|
||||
// Sigils may be reversible (encoding, compression, encryption) or irreversible (hashing).
|
||||
//
|
||||
// For reversible sigils: Out(In(x)) == x for all valid x
|
||||
// For irreversible sigils: Out returns the input unchanged
|
||||
// For symmetric sigils: In(x) == Out(x)
|
||||
//
|
||||
// Implementations must handle nil input by returning nil without error,
|
||||
// and empty input by returning an empty slice without error.
|
||||
type Sigil interface {
|
||||
// In transforms the data.
|
||||
// In applies the forward transformation to the data.
|
||||
// For encoding sigils, this encodes the data.
|
||||
// For compression sigils, this compresses the data.
|
||||
// For hash sigils, this computes the digest.
|
||||
In(data []byte) ([]byte, error)
|
||||
// Out reverses the transformation.
|
||||
|
||||
// Out applies the reverse transformation to the data.
|
||||
// For reversible sigils, this recovers the original data.
|
||||
// For irreversible sigils (e.g., hashing), this returns the input unchanged.
|
||||
Out(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
|
|
@ -14,7 +41,13 @@ type Enchantrix interface {
|
|||
Transmute(data []byte, sigils []Sigil) ([]byte, error)
|
||||
}
|
||||
|
||||
// Transmute is a helper function for applying a series of sigils to data.
|
||||
// Transmute applies a series of sigils to data in sequence.
|
||||
//
|
||||
// Each sigil's In method is called in order, with the output of one sigil
|
||||
// becoming the input of the next. If any sigil returns an error, Transmute
|
||||
// stops immediately and returns nil with that error.
|
||||
//
|
||||
// To reverse a transmutation, call each sigil's Out method in reverse order.
|
||||
func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
|
||||
var err error
|
||||
for _, sigil := range sigils {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,23 @@
|
|||
// Package trix implements the TRIX binary container format (RFC-0002).
|
||||
//
|
||||
// The .trix format is a generic, protocol-agnostic container for storing
|
||||
// arbitrary binary payloads alongside structured JSON metadata. It consists of:
|
||||
//
|
||||
// [Magic Number (4)] [Version (1)] [Header Length (4)] [JSON Header] [Payload]
|
||||
//
|
||||
// Key features:
|
||||
// - Custom 4-byte magic number for application-specific identification
|
||||
// - Extensible JSON header for metadata (content type, checksums, timestamps)
|
||||
// - Optional integrity verification via configurable checksum algorithms
|
||||
// - Integration with the Sigil transformation framework for encoding/compression
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// container := &trix.Trix{
|
||||
// Header: map[string]interface{}{"content_type": "text/plain"},
|
||||
// Payload: []byte("Hello, World!"),
|
||||
// }
|
||||
// encoded, _ := trix.Encode(container, "MYAP", nil)
|
||||
package trix
|
||||
|
||||
import (
|
||||
|
|
@ -14,8 +34,10 @@ import (
|
|||
|
||||
const (
|
||||
// Version is the current version of the .trix file format.
|
||||
// See RFC-0002 for version history and compatibility notes.
|
||||
Version = 2
|
||||
// MaxHeaderSize is the maximum allowed size for the header.
|
||||
// MaxHeaderSize is the maximum allowed size for the header (16 MB).
|
||||
// This limit prevents denial-of-service attacks via large header allocations.
|
||||
MaxHeaderSize = 16 * 1024 * 1024 // 16 MB
|
||||
)
|
||||
|
||||
|
|
@ -34,13 +56,29 @@ var (
|
|||
ErrHeaderTooLarge = errors.New("trix: header size exceeds maximum allowed")
|
||||
)
|
||||
|
||||
// Trix represents the structure of a .trix file.
|
||||
// It contains a header, a payload, and optional sigils for data transformation.
|
||||
// Trix represents a .trix container with header metadata and binary payload.
|
||||
//
|
||||
// The Header field holds arbitrary JSON-serializable metadata. Common fields include:
|
||||
// - content_type: MIME type of the original payload
|
||||
// - created_at: ISO 8601 timestamp
|
||||
// - encryption_algorithm: Algorithm used for encryption (if applicable)
|
||||
// - checksum: Hex-encoded integrity checksum (auto-populated if ChecksumAlgo is set)
|
||||
//
|
||||
// The InSigils and OutSigils fields specify transformation pipelines:
|
||||
// - InSigils: Applied during Pack() in order (e.g., ["gzip", "base64"])
|
||||
// - OutSigils: Applied during Unpack() in reverse order (defaults to InSigils)
|
||||
type Trix struct {
|
||||
Header map[string]interface{}
|
||||
Payload []byte
|
||||
InSigils []string `json:"-"` // Ignore Sigils during JSON marshaling
|
||||
OutSigils []string `json:"-"` // Ignore Sigils during JSON marshaling
|
||||
// Header contains JSON-serializable metadata about the payload.
|
||||
Header map[string]interface{}
|
||||
// Payload is the binary data stored in the container.
|
||||
Payload []byte
|
||||
// InSigils lists sigil names to apply during Pack (forward transformation).
|
||||
InSigils []string `json:"-"`
|
||||
// OutSigils lists sigil names to apply during Unpack (reverse transformation).
|
||||
// If empty, InSigils is used in reverse order.
|
||||
OutSigils []string `json:"-"`
|
||||
// ChecksumAlgo specifies the hash algorithm for integrity verification.
|
||||
// If set, a checksum is computed and stored in the header during Encode.
|
||||
ChecksumAlgo crypt.HashType `json:"-"`
|
||||
}
|
||||
|
||||
|
|
|
|||
364
rfcs/RFC-0001-Pre-Obfuscation-Layer.md
Normal file
364
rfcs/RFC-0001-Pre-Obfuscation-Layer.md
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
# RFC-0001: Pre-Obfuscation Layer Protocol for AEAD Ciphers
|
||||
|
||||
**Status:** Informational
|
||||
**Version:** 1.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies a pre-obfuscation layer protocol designed to transform plaintext data before it reaches CPU encryption routines. The protocol provides an additional security layer that prevents raw plaintext patterns from being processed directly by encryption hardware, mitigating potential side-channel attack vectors while maintaining full compatibility with standard AEAD cipher constructions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Protocol Overview](#3-protocol-overview)
|
||||
4. [Obfuscator Implementations](#4-obfuscator-implementations)
|
||||
5. [Integration with AEAD Ciphers](#5-integration-with-aead-ciphers)
|
||||
6. [Wire Format](#6-wire-format)
|
||||
7. [Security Considerations](#7-security-considerations)
|
||||
8. [Implementation Requirements](#8-implementation-requirements)
|
||||
9. [Test Vectors](#9-test-vectors)
|
||||
10. [References](#10-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Modern AEAD (Authenticated Encryption with Associated Data) ciphers like ChaCha20-Poly1305 and AES-GCM provide strong cryptographic guarantees. However, the plaintext data is processed directly by CPU encryption instructions, potentially exposing patterns through side-channel attacks such as timing analysis, power analysis, or electromagnetic emanation.
|
||||
|
||||
This RFC defines a pre-obfuscation layer that transforms plaintext into an unpredictable byte sequence before encryption. The transformation is reversible, deterministic (given the same entropy source), and adds negligible overhead while providing defense-in-depth against side-channel attacks.
|
||||
|
||||
### 1.1 Design Goals
|
||||
|
||||
- **Reversibility**: All transformations MUST be perfectly reversible
|
||||
- **Determinism**: Given the same entropy, transformations MUST produce identical results
|
||||
- **Independence**: The obfuscation layer operates independently of the underlying cipher
|
||||
- **Zero overhead on security**: The underlying AEAD cipher's security properties are preserved
|
||||
- **Minimal computational overhead**: Transformations should add < 5% processing time
|
||||
|
||||
## 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.
|
||||
|
||||
**Plaintext**: The original data to be encrypted
|
||||
**Obfuscated data**: Plaintext after pre-obfuscation transformation
|
||||
**Ciphertext**: Obfuscated data after encryption
|
||||
**Entropy**: A source of randomness used to derive transformation parameters (typically the nonce)
|
||||
**Key stream**: A deterministic sequence of bytes derived from entropy
|
||||
**Permutation**: A bijective mapping of byte positions
|
||||
|
||||
## 3. Protocol Overview
|
||||
|
||||
The pre-obfuscation protocol operates in two stages:
|
||||
|
||||
### 3.1 Encryption Flow
|
||||
|
||||
```
|
||||
Plaintext --> Obfuscate(plaintext, entropy) --> Obfuscated --> Encrypt --> Ciphertext
|
||||
```
|
||||
|
||||
1. Generate cryptographic nonce for the AEAD cipher
|
||||
2. Apply obfuscation transformation using nonce as entropy
|
||||
3. Encrypt the obfuscated data using the AEAD cipher
|
||||
4. Output: `[nonce || ciphertext || auth_tag]`
|
||||
|
||||
### 3.2 Decryption Flow
|
||||
|
||||
```
|
||||
Ciphertext --> Decrypt --> Obfuscated --> Deobfuscate(obfuscated, entropy) --> Plaintext
|
||||
```
|
||||
|
||||
1. Extract nonce from the ciphertext prefix
|
||||
2. Decrypt the ciphertext using the AEAD cipher
|
||||
3. Apply reverse obfuscation transformation using the extracted nonce
|
||||
4. Output: Original plaintext
|
||||
|
||||
### 3.3 Entropy Derivation
|
||||
|
||||
The entropy source MUST be the same value used as the AEAD cipher nonce. This ensures:
|
||||
|
||||
- No additional random values need to be generated or stored
|
||||
- The obfuscation is tied to the specific encryption operation
|
||||
- Replay of ciphertext with different obfuscation is not possible
|
||||
|
||||
## 4. Obfuscator Implementations
|
||||
|
||||
This RFC defines two standard obfuscator implementations. Implementations MAY support additional obfuscators provided they meet the requirements in Section 8.
|
||||
|
||||
### 4.1 XOR Obfuscator
|
||||
|
||||
The XOR obfuscator generates a deterministic key stream from the entropy and XORs it with the plaintext.
|
||||
|
||||
#### 4.1.1 Key Stream Derivation
|
||||
|
||||
```
|
||||
function deriveKeyStream(entropy: bytes, length: int) -> bytes:
|
||||
stream = empty byte array of size length
|
||||
blockNum = 0
|
||||
offset = 0
|
||||
|
||||
while offset < length:
|
||||
block = SHA256(entropy || BigEndian64(blockNum))
|
||||
copyLen = min(32, length - offset)
|
||||
copy block[0:copyLen] to stream[offset:offset+copyLen]
|
||||
offset += copyLen
|
||||
blockNum += 1
|
||||
|
||||
return stream
|
||||
```
|
||||
|
||||
#### 4.1.2 Obfuscation
|
||||
|
||||
```
|
||||
function obfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
if length(data) == 0:
|
||||
return data
|
||||
|
||||
keyStream = deriveKeyStream(entropy, length(data))
|
||||
result = new byte array of size length(data)
|
||||
|
||||
for i = 0 to length(data) - 1:
|
||||
result[i] = data[i] XOR keyStream[i]
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
#### 4.1.3 Deobfuscation
|
||||
|
||||
The XOR operation is symmetric; deobfuscation uses the same algorithm:
|
||||
|
||||
```
|
||||
function deobfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
return obfuscate(data, entropy) // XOR is self-inverse
|
||||
```
|
||||
|
||||
### 4.2 Shuffle-Mask Obfuscator
|
||||
|
||||
The shuffle-mask obfuscator provides additional diffusion by combining a byte-level shuffle with an XOR mask.
|
||||
|
||||
#### 4.2.1 Permutation Generation
|
||||
|
||||
Uses Fisher-Yates shuffle with deterministic randomness:
|
||||
|
||||
```
|
||||
function generatePermutation(entropy: bytes, length: int) -> int[]:
|
||||
perm = [0, 1, 2, ..., length-1]
|
||||
seed = SHA256(entropy || "permutation")
|
||||
|
||||
for i = length-1 downto 1:
|
||||
hash = SHA256(seed || BigEndian64(i))
|
||||
j = BigEndian64(hash[0:8]) mod (i + 1)
|
||||
swap perm[i] and perm[j]
|
||||
|
||||
return perm
|
||||
```
|
||||
|
||||
#### 4.2.2 Mask Derivation
|
||||
|
||||
```
|
||||
function deriveMask(entropy: bytes, length: int) -> bytes:
|
||||
mask = empty byte array of size length
|
||||
blockNum = 0
|
||||
offset = 0
|
||||
|
||||
while offset < length:
|
||||
block = SHA256(entropy || "mask" || BigEndian64(blockNum))
|
||||
copyLen = min(32, length - offset)
|
||||
copy block[0:copyLen] to mask[offset:offset+copyLen]
|
||||
offset += copyLen
|
||||
blockNum += 1
|
||||
|
||||
return mask
|
||||
```
|
||||
|
||||
#### 4.2.3 Obfuscation
|
||||
|
||||
```
|
||||
function obfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
if length(data) == 0:
|
||||
return data
|
||||
|
||||
perm = generatePermutation(entropy, length(data))
|
||||
mask = deriveMask(entropy, length(data))
|
||||
|
||||
// Step 1: Apply mask
|
||||
masked = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
masked[i] = data[i] XOR mask[i]
|
||||
|
||||
// Step 2: Shuffle bytes according to permutation
|
||||
shuffled = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
shuffled[i] = masked[perm[i]]
|
||||
|
||||
return shuffled
|
||||
```
|
||||
|
||||
#### 4.2.4 Deobfuscation
|
||||
|
||||
```
|
||||
function deobfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
if length(data) == 0:
|
||||
return data
|
||||
|
||||
perm = generatePermutation(entropy, length(data))
|
||||
mask = deriveMask(entropy, length(data))
|
||||
|
||||
// Step 1: Unshuffle bytes (inverse permutation)
|
||||
unshuffled = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
unshuffled[perm[i]] = data[i]
|
||||
|
||||
// Step 2: Remove mask
|
||||
result = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
result[i] = unshuffled[i] XOR mask[i]
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
## 5. Integration with AEAD Ciphers
|
||||
|
||||
### 5.1 XChaCha20-Poly1305 Integration
|
||||
|
||||
When used with XChaCha20-Poly1305:
|
||||
|
||||
- Nonce size: 24 bytes
|
||||
- Key size: 32 bytes
|
||||
- Auth tag size: 16 bytes
|
||||
|
||||
```
|
||||
function encrypt(key: bytes[32], plaintext: bytes) -> bytes:
|
||||
nonce = random_bytes(24)
|
||||
obfuscated = obfuscator.obfuscate(plaintext, nonce)
|
||||
ciphertext = XChaCha20Poly1305_Seal(key, nonce, obfuscated, nil)
|
||||
return nonce || ciphertext // nonce is prepended
|
||||
```
|
||||
|
||||
```
|
||||
function decrypt(key: bytes[32], data: bytes) -> bytes:
|
||||
if length(data) < 24 + 16: // nonce + auth tag minimum
|
||||
return error("ciphertext too short")
|
||||
|
||||
nonce = data[0:24]
|
||||
ciphertext = data[24:]
|
||||
obfuscated = XChaCha20Poly1305_Open(key, nonce, ciphertext, nil)
|
||||
plaintext = obfuscator.deobfuscate(obfuscated, nonce)
|
||||
return plaintext
|
||||
```
|
||||
|
||||
### 5.2 Other AEAD Ciphers
|
||||
|
||||
The pre-obfuscation layer is cipher-agnostic. For other AEAD ciphers:
|
||||
|
||||
| Cipher | Nonce Size | Notes |
|
||||
|--------|------------|-------|
|
||||
| AES-128-GCM | 12 bytes | Standard nonce |
|
||||
| AES-256-GCM | 12 bytes | Standard nonce |
|
||||
| ChaCha20-Poly1305 | 12 bytes | Original ChaCha nonce |
|
||||
| XChaCha20-Poly1305 | 24 bytes | Extended nonce (RECOMMENDED) |
|
||||
|
||||
## 6. Wire Format
|
||||
|
||||
The output wire format is:
|
||||
|
||||
```
|
||||
+----------------+------------------------+
|
||||
| Nonce | Ciphertext |
|
||||
+----------------+------------------------+
|
||||
| N bytes | len(plaintext) + T |
|
||||
```
|
||||
|
||||
Where:
|
||||
- `N` = Nonce size (cipher-dependent)
|
||||
- `T` = Authentication tag size (typically 16 bytes)
|
||||
|
||||
The obfuscation parameters are NOT stored in the wire format. They are derived deterministically from the nonce.
|
||||
|
||||
## 7. Security Considerations
|
||||
|
||||
### 7.1 Side-Channel Mitigation
|
||||
|
||||
The pre-obfuscation layer provides defense-in-depth against:
|
||||
|
||||
- **Timing attacks**: Plaintext patterns do not influence encryption timing
|
||||
- **Cache-timing attacks**: Memory access patterns are decorrelated from plaintext
|
||||
- **Power analysis**: Power consumption patterns are decorrelated from plaintext structure
|
||||
|
||||
### 7.2 Cryptographic Security
|
||||
|
||||
The pre-obfuscation layer does NOT provide cryptographic security on its own. It MUST always be used in conjunction with a proper AEAD cipher. The security of the combined system relies entirely on the underlying AEAD cipher's security guarantees.
|
||||
|
||||
### 7.3 Entropy Requirements
|
||||
|
||||
The entropy source (nonce) MUST be generated using a cryptographically secure random number generator. Nonce reuse with the same key compromises both the obfuscation determinism and the AEAD security.
|
||||
|
||||
### 7.4 Key Stream Exhaustion
|
||||
|
||||
The XOR obfuscator uses SHA-256 in counter mode. For a single encryption:
|
||||
- Maximum safely obfuscated data: 2^64 * 32 bytes (theoretical)
|
||||
- Practical limit: Constrained by AEAD cipher limits
|
||||
|
||||
### 7.5 Permutation Uniqueness
|
||||
|
||||
The shuffle-mask obfuscator generates permutations deterministically. For data of length `n`:
|
||||
- Total possible permutations: n!
|
||||
- Entropy required for full permutation space: log2(n!) bits
|
||||
- SHA-256 provides 256 bits, sufficient for n up to ~57 bytes without collision concerns
|
||||
|
||||
For larger data, the permutation space is sampled uniformly but not exhaustively.
|
||||
|
||||
## 8. Implementation Requirements
|
||||
|
||||
Conforming implementations MUST:
|
||||
|
||||
1. Support at least the XOR obfuscator
|
||||
2. Use SHA-256 for key stream and permutation derivation
|
||||
3. Use big-endian byte ordering for block numbers
|
||||
4. Handle zero-length data by returning it unchanged
|
||||
5. Prepend the nonce to the ciphertext output
|
||||
6. Accept and process the nonce from ciphertext prefix during decryption
|
||||
|
||||
Conforming implementations SHOULD:
|
||||
|
||||
1. Support the shuffle-mask obfuscator
|
||||
2. Use XChaCha20-Poly1305 as the default AEAD cipher
|
||||
3. Provide constant-time implementations where feasible
|
||||
|
||||
## 9. Test Vectors
|
||||
|
||||
### 9.1 XOR Obfuscator
|
||||
|
||||
```
|
||||
Entropy (hex): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
|
||||
Plaintext (hex): 48656c6c6f2c20576f726c6421
|
||||
Expected key stream prefix (hex): [first 14 bytes of SHA256(entropy || 0x0000000000000000)]
|
||||
```
|
||||
|
||||
### 9.2 Shuffle-Mask Obfuscator
|
||||
|
||||
```
|
||||
Entropy (hex): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
|
||||
Plaintext: "Hello"
|
||||
Permutation seed: SHA256(entropy || "permutation")
|
||||
Mask seed: SHA256(entropy || "mask" || 0x0000000000000000)
|
||||
```
|
||||
|
||||
## 10. References
|
||||
|
||||
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
|
||||
- [RFC 7539] ChaCha20 and Poly1305 for IETF Protocols (obsoleted by 8439)
|
||||
- [draft-irtf-cfrg-xchacha] XChaCha: eXtended-nonce ChaCha and AEAD_XChaCha20_Poly1305
|
||||
- [FIPS 180-4] Secure Hash Standard (SHA-256)
|
||||
- Fisher, R. A.; Yates, F. (1948). Statistical tables for biological, agricultural and medical research
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
`github.com/Snider/Enchantrix/pkg/enchantrix/crypto_sigil.go`
|
||||
|
||||
## Appendix B: Changelog
|
||||
|
||||
- **1.0** (2025-01-13): Initial specification
|
||||
414
rfcs/RFC-0002-Trix-Container-Format.md
Normal file
414
rfcs/RFC-0002-Trix-Container-Format.md
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
# RFC-0002: TRIX Binary Container Format
|
||||
|
||||
**Status:** Standards Track
|
||||
**Version:** 2.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the TRIX binary container format, a generic and extensible file format designed to store arbitrary binary payloads alongside structured JSON metadata. The format is protocol-agnostic, supporting any encryption scheme, compression algorithm, or data transformation while providing a consistent structure for metadata discovery and payload extraction.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Format Specification](#3-format-specification)
|
||||
4. [Header Specification](#4-header-specification)
|
||||
5. [Encoding Process](#5-encoding-process)
|
||||
6. [Decoding Process](#6-decoding-process)
|
||||
7. [Checksum Verification](#7-checksum-verification)
|
||||
8. [Magic Number Registry](#8-magic-number-registry)
|
||||
9. [Security Considerations](#9-security-considerations)
|
||||
10. [IANA Considerations](#10-iana-considerations)
|
||||
11. [References](#11-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
The TRIX format addresses the need for a simple, self-describing binary container that can wrap any payload type with extensible metadata. Unlike format-specific containers (such as encrypted archive formats), TRIX separates the concerns of:
|
||||
|
||||
- **Container structure**: How data is organized on disk/wire
|
||||
- **Payload semantics**: What the payload contains and how to process it
|
||||
- **Metadata extensibility**: Application-specific attributes
|
||||
|
||||
### 1.1 Design Goals
|
||||
|
||||
- **Simplicity**: Minimal overhead, easy to implement
|
||||
- **Extensibility**: JSON header allows arbitrary metadata
|
||||
- **Protocol-agnostic**: No assumptions about payload encryption or encoding
|
||||
- **Streaming-friendly**: Header length prefix enables streaming reads
|
||||
- **Magic-number customizable**: Applications can define their own identifiers
|
||||
|
||||
### 1.2 Use Cases
|
||||
|
||||
- Encrypted data interchange
|
||||
- Signed document containers
|
||||
- Configuration file packaging
|
||||
- Backup archive format
|
||||
- Inter-service message envelopes
|
||||
|
||||
## 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.
|
||||
|
||||
**Container**: A complete TRIX-formatted byte sequence
|
||||
**Magic Number**: A 4-byte identifier at the start of the container
|
||||
**Header**: A JSON object containing metadata about the payload
|
||||
**Payload**: The arbitrary binary data stored in the container
|
||||
**Checksum**: An optional integrity verification value
|
||||
|
||||
## 3. Format Specification
|
||||
|
||||
### 3.1 Overview
|
||||
|
||||
A TRIX container consists of five sequential fields:
|
||||
|
||||
```
|
||||
+----------------+---------+---------------+----------------+-----------+
|
||||
| Magic Number | Version | Header Length | JSON Header | Payload |
|
||||
+----------------+---------+---------------+----------------+-----------+
|
||||
| 4 bytes | 1 byte | 4 bytes | Variable | Variable |
|
||||
```
|
||||
|
||||
Total minimum size: 9 bytes (empty header, empty payload)
|
||||
|
||||
### 3.2 Field Definitions
|
||||
|
||||
#### 3.2.1 Magic Number (4 bytes)
|
||||
|
||||
A 4-byte ASCII string identifying the file type. This field:
|
||||
|
||||
- MUST be exactly 4 bytes
|
||||
- SHOULD contain printable ASCII characters
|
||||
- Is application-defined (not mandated by this specification)
|
||||
|
||||
Common conventions:
|
||||
- `TRIX` - Generic TRIX container
|
||||
- First character uppercase, application-specific identifier
|
||||
|
||||
#### 3.2.2 Version (1 byte)
|
||||
|
||||
An unsigned 8-bit integer indicating the format version.
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| 0x00 | Reserved |
|
||||
| 0x01 | Version 1.0 (deprecated) |
|
||||
| 0x02 | Version 2.0 (current) |
|
||||
| 0x03-0xFF | Reserved for future versions |
|
||||
|
||||
Implementations MUST reject containers with unrecognized versions.
|
||||
|
||||
#### 3.2.3 Header Length (4 bytes)
|
||||
|
||||
A 32-bit unsigned integer in big-endian byte order specifying the length of the JSON Header in bytes.
|
||||
|
||||
- Minimum value: 0 (empty header represented as `{}` is 2 bytes, but 0 is valid)
|
||||
- Maximum value: 16,777,215 (16 MB - 1 byte)
|
||||
|
||||
Implementations MUST reject headers exceeding 16 MB to prevent denial-of-service attacks.
|
||||
|
||||
```
|
||||
Header Length = BigEndian32(length_of_json_header_bytes)
|
||||
```
|
||||
|
||||
#### 3.2.4 JSON Header (Variable)
|
||||
|
||||
A UTF-8 encoded JSON object containing metadata. The header:
|
||||
|
||||
- MUST be valid JSON (RFC 8259)
|
||||
- MUST be a JSON object (not array, string, or primitive)
|
||||
- SHOULD use UTF-8 encoding without BOM
|
||||
- MAY be empty (`{}`)
|
||||
|
||||
#### 3.2.5 Payload (Variable)
|
||||
|
||||
The arbitrary binary payload. The payload:
|
||||
|
||||
- MAY be empty (zero bytes)
|
||||
- MAY contain any binary data
|
||||
- Length is implicitly determined by: `container_length - 9 - header_length`
|
||||
|
||||
## 4. Header Specification
|
||||
|
||||
### 4.1 Reserved Header Fields
|
||||
|
||||
The following header fields have defined semantics:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `content_type` | string | MIME type of the payload (before any transformations) |
|
||||
| `checksum` | string | Hex-encoded checksum of the payload |
|
||||
| `checksum_algo` | string | Algorithm used for checksum (e.g., "sha256") |
|
||||
| `created_at` | string | ISO 8601 timestamp of creation |
|
||||
| `encryption_algorithm` | string | Encryption algorithm identifier |
|
||||
| `compression` | string | Compression algorithm identifier |
|
||||
| `sigils` | array | Ordered list of transformation sigil names |
|
||||
|
||||
### 4.2 Extension Fields
|
||||
|
||||
Applications MAY include additional fields. To avoid conflicts:
|
||||
|
||||
- Custom fields SHOULD use a namespace prefix (e.g., `x-myapp-field`)
|
||||
- Standard field names are lowercase with underscores
|
||||
|
||||
### 4.3 Example Headers
|
||||
|
||||
#### Encrypted payload:
|
||||
```json
|
||||
{
|
||||
"content_type": "application/octet-stream",
|
||||
"encryption_algorithm": "xchacha20poly1305",
|
||||
"created_at": "2025-01-13T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Compressed and encoded payload:
|
||||
```json
|
||||
{
|
||||
"content_type": "text/plain",
|
||||
"compression": "gzip",
|
||||
"sigils": ["gzip", "base64"],
|
||||
"checksum": "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e",
|
||||
"checksum_algo": "sha256"
|
||||
}
|
||||
```
|
||||
|
||||
#### Minimal header:
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
## 5. Encoding Process
|
||||
|
||||
### 5.1 Algorithm
|
||||
|
||||
```
|
||||
function Encode(payload: bytes, header: object, magic: string) -> bytes:
|
||||
// Validate magic number
|
||||
if length(magic) != 4:
|
||||
return error("magic number must be 4 bytes")
|
||||
|
||||
// Serialize header to JSON
|
||||
header_bytes = JSON.serialize(header)
|
||||
header_length = length(header_bytes)
|
||||
|
||||
// Validate header size
|
||||
if header_length > 16777215:
|
||||
return error("header exceeds maximum size")
|
||||
|
||||
// Build container
|
||||
container = empty byte buffer
|
||||
|
||||
// Write magic number (4 bytes)
|
||||
container.write(magic)
|
||||
|
||||
// Write version (1 byte)
|
||||
container.write(0x02)
|
||||
|
||||
// Write header length (4 bytes, big-endian)
|
||||
container.write(BigEndian32(header_length))
|
||||
|
||||
// Write JSON header
|
||||
container.write(header_bytes)
|
||||
|
||||
// Write payload
|
||||
container.write(payload)
|
||||
|
||||
return container.bytes()
|
||||
```
|
||||
|
||||
### 5.2 Checksum Integration
|
||||
|
||||
If integrity verification is required:
|
||||
|
||||
```
|
||||
function EncodeWithChecksum(payload: bytes, header: object, magic: string, algo: string) -> bytes:
|
||||
checksum = Hash(algo, payload)
|
||||
header["checksum"] = HexEncode(checksum)
|
||||
header["checksum_algo"] = algo
|
||||
return Encode(payload, header, magic)
|
||||
```
|
||||
|
||||
## 6. Decoding Process
|
||||
|
||||
### 6.1 Algorithm
|
||||
|
||||
```
|
||||
function Decode(container: bytes, expected_magic: string) -> (header: object, payload: bytes):
|
||||
// Validate minimum size
|
||||
if length(container) < 9:
|
||||
return error("container too small")
|
||||
|
||||
// Read and verify magic number
|
||||
magic = container[0:4]
|
||||
if magic != expected_magic:
|
||||
return error("invalid magic number")
|
||||
|
||||
// Read and verify version
|
||||
version = container[4]
|
||||
if version != 0x02:
|
||||
return error("unsupported version")
|
||||
|
||||
// Read header length
|
||||
header_length = BigEndian32(container[5:9])
|
||||
|
||||
// Validate header length
|
||||
if header_length > 16777215:
|
||||
return error("header length exceeds maximum")
|
||||
|
||||
if length(container) < 9 + header_length:
|
||||
return error("container truncated")
|
||||
|
||||
// Read and parse header
|
||||
header_bytes = container[9:9+header_length]
|
||||
header = JSON.parse(header_bytes)
|
||||
|
||||
// Read payload
|
||||
payload = container[9+header_length:]
|
||||
|
||||
return (header, payload)
|
||||
```
|
||||
|
||||
### 6.2 Streaming Decode
|
||||
|
||||
For large files, streaming decode is RECOMMENDED:
|
||||
|
||||
```
|
||||
function StreamDecode(reader: Reader, expected_magic: string) -> (header: object, payload_reader: Reader):
|
||||
// Read fixed-size prefix
|
||||
prefix = reader.read(9)
|
||||
|
||||
// Validate magic and version
|
||||
magic = prefix[0:4]
|
||||
version = prefix[4]
|
||||
header_length = BigEndian32(prefix[5:9])
|
||||
|
||||
// Read header
|
||||
header_bytes = reader.read(header_length)
|
||||
header = JSON.parse(header_bytes)
|
||||
|
||||
// Return remaining reader for payload streaming
|
||||
return (header, reader)
|
||||
```
|
||||
|
||||
## 7. Checksum Verification
|
||||
|
||||
### 7.1 Supported Algorithms
|
||||
|
||||
| Algorithm ID | Output Size | Notes |
|
||||
|--------------|-------------|-------|
|
||||
| `md5` | 16 bytes | NOT RECOMMENDED for security |
|
||||
| `sha1` | 20 bytes | NOT RECOMMENDED for security |
|
||||
| `sha256` | 32 bytes | RECOMMENDED |
|
||||
| `sha384` | 48 bytes | |
|
||||
| `sha512` | 64 bytes | |
|
||||
| `blake2b-256` | 32 bytes | |
|
||||
| `blake2b-512` | 64 bytes | |
|
||||
|
||||
### 7.2 Verification Process
|
||||
|
||||
```
|
||||
function VerifyChecksum(header: object, payload: bytes) -> bool:
|
||||
if "checksum" not in header:
|
||||
return true // No checksum to verify
|
||||
|
||||
algo = header["checksum_algo"]
|
||||
expected = HexDecode(header["checksum"])
|
||||
actual = Hash(algo, payload)
|
||||
|
||||
return constant_time_compare(expected, actual)
|
||||
```
|
||||
|
||||
## 8. Magic Number Registry
|
||||
|
||||
This section defines conventions for magic number allocation:
|
||||
|
||||
### 8.1 Reserved Magic Numbers
|
||||
|
||||
| Magic | Reserved For |
|
||||
|-------|--------------|
|
||||
| `TRIX` | Generic TRIX containers |
|
||||
| `\x00\x00\x00\x00` | Reserved (null) |
|
||||
| `\xFF\xFF\xFF\xFF` | Reserved (test/invalid) |
|
||||
|
||||
### 8.2 Allocation Guidelines
|
||||
|
||||
Applications SHOULD:
|
||||
|
||||
1. Use 4 printable ASCII characters
|
||||
2. Start with an uppercase letter
|
||||
3. Avoid common file format magic numbers (e.g., `%PDF`, `PK\x03\x04`)
|
||||
4. Register custom magic numbers in their documentation
|
||||
|
||||
## 9. Security Considerations
|
||||
|
||||
### 9.1 Header Injection
|
||||
|
||||
The JSON header is parsed before processing. Implementations MUST:
|
||||
|
||||
- Validate JSON syntax strictly
|
||||
- Reject headers with duplicate keys
|
||||
- Not execute header field values as code
|
||||
|
||||
### 9.2 Denial of Service
|
||||
|
||||
The 16 MB header limit prevents memory exhaustion attacks. Implementations SHOULD:
|
||||
|
||||
- Reject headers before full allocation if length exceeds limit
|
||||
- Implement timeouts for header parsing
|
||||
- Limit recursion depth in JSON parsing
|
||||
|
||||
### 9.3 Path Traversal
|
||||
|
||||
Header fields like `filename` MUST NOT be used directly for filesystem operations without sanitization.
|
||||
|
||||
### 9.4 Checksum Security
|
||||
|
||||
- MD5 and SHA1 checksums provide integrity but not authenticity
|
||||
- For tamper detection, use HMAC or digital signatures
|
||||
- Checksum verification MUST use constant-time comparison
|
||||
|
||||
### 9.5 Version Negotiation
|
||||
|
||||
Implementations MUST NOT attempt to parse containers with unknown versions, as the format may change incompatibly.
|
||||
|
||||
## 10. IANA Considerations
|
||||
|
||||
This document does not require IANA actions. The TRIX format is application-defined and does not use IANA-managed namespaces.
|
||||
|
||||
Future versions may define:
|
||||
- Media type registration (e.g., `application/x-trix`)
|
||||
- Magic number registry
|
||||
|
||||
## 11. References
|
||||
|
||||
- [RFC 8259] The JavaScript Object Notation (JSON) Data Interchange Format
|
||||
- [RFC 2119] Key words for use in RFCs to Indicate Requirement Levels
|
||||
- [RFC 6838] Media Type Specifications and Registration Procedures
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Binary Layout Diagram
|
||||
|
||||
```
|
||||
Byte offset: 0 4 5 9 9+H 9+H+P
|
||||
|---------|----|---------|---------|---------|
|
||||
| Magic | V | HdrLen | Header | Payload |
|
||||
| (4) |(1) | (4) | (H) | (P) |
|
||||
|---------|----|---------|---------|---------|
|
||||
|
||||
V = Version byte
|
||||
H = Header length (from HdrLen field)
|
||||
P = Payload length (remaining bytes)
|
||||
```
|
||||
|
||||
## Appendix B: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
`github.com/Snider/Enchantrix/pkg/trix/trix.go`
|
||||
|
||||
## Appendix C: Changelog
|
||||
|
||||
- **2.0** (2025-01-13): Current version with JSON header
|
||||
- **1.0** (deprecated): Initial version with fixed header fields
|
||||
505
rfcs/RFC-0003-Sigil-Transformation-Framework.md
Normal file
505
rfcs/RFC-0003-Sigil-Transformation-Framework.md
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
# RFC-0003: Sigil Transformation Framework
|
||||
|
||||
**Status:** Standards Track
|
||||
**Version:** 1.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the Sigil Transformation Framework, a composable interface for defining reversible and irreversible data transformations. Sigils provide a uniform abstraction for encoding, compression, hashing, encryption, and other byte-level operations, enabling declarative transformation pipelines that can be applied and reversed systematically.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Interface Specification](#3-interface-specification)
|
||||
4. [Sigil Categories](#4-sigil-categories)
|
||||
5. [Standard Sigils](#5-standard-sigils)
|
||||
6. [Composition and Chaining](#6-composition-and-chaining)
|
||||
7. [Error Handling](#7-error-handling)
|
||||
8. [Implementation Guidelines](#8-implementation-guidelines)
|
||||
9. [Security Considerations](#9-security-considerations)
|
||||
10. [References](#10-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Data transformation is a fundamental operation in software systems. Common transformations include:
|
||||
|
||||
- **Encoding**: Converting between representations (hex, base64)
|
||||
- **Compression**: Reducing data size (gzip, zstd)
|
||||
- **Encryption**: Protecting confidentiality (AES, ChaCha20)
|
||||
- **Hashing**: Computing digests (SHA-256, BLAKE2)
|
||||
- **Formatting**: Restructuring data (JSON minification)
|
||||
|
||||
The Sigil framework provides a uniform interface for all these operations, enabling:
|
||||
|
||||
- Declarative transformation pipelines
|
||||
- Automatic reversal of transformation chains
|
||||
- Composable, reusable transformation units
|
||||
- Clear semantics for reversible vs. irreversible operations
|
||||
|
||||
### 1.1 Design Principles
|
||||
|
||||
1. **Simplicity**: Two methods, clear contract
|
||||
2. **Composability**: Sigils combine naturally
|
||||
3. **Reversibility awareness**: Explicit handling of one-way operations
|
||||
4. **Null safety**: Defined behavior for nil/empty inputs
|
||||
5. **Error propagation**: Clear error semantics
|
||||
|
||||
## 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.
|
||||
|
||||
**Sigil**: A transformation unit implementing the Sigil interface
|
||||
**In operation**: The forward transformation (encode, compress, encrypt, hash)
|
||||
**Out operation**: The reverse transformation (decode, decompress, decrypt)
|
||||
**Reversible sigil**: A sigil where Out(In(x)) = x for all valid x
|
||||
**Irreversible sigil**: A sigil where Out returns the input unchanged or errors
|
||||
**Symmetric sigil**: A sigil where In(x) = Out(x) (e.g., byte reversal)
|
||||
**Transmutation**: Applying a sequence of sigils to data
|
||||
|
||||
## 3. Interface Specification
|
||||
|
||||
### 3.1 Sigil Interface
|
||||
|
||||
```
|
||||
interface Sigil {
|
||||
// In transforms the data (forward operation).
|
||||
// Returns transformed data and any error encountered.
|
||||
In(data: bytes) -> (bytes, error)
|
||||
|
||||
// Out reverses the transformation (reverse operation).
|
||||
// For irreversible sigils, returns data unchanged.
|
||||
Out(data: bytes) -> (bytes, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Method Contracts
|
||||
|
||||
#### 3.2.1 In Method
|
||||
|
||||
The `In` method MUST:
|
||||
|
||||
- Accept a byte slice as input
|
||||
- Return a byte slice as output
|
||||
- Return nil output for nil input (without error)
|
||||
- Return empty slice for empty input (without error)
|
||||
- Return an error if transformation fails
|
||||
|
||||
#### 3.2.2 Out Method
|
||||
|
||||
The `Out` method MUST:
|
||||
|
||||
- Accept a byte slice as input
|
||||
- Return a byte slice as output
|
||||
- Return nil output for nil input (without error)
|
||||
- Return empty slice for empty input (without error)
|
||||
- For reversible sigils: return the original data before `In` was applied
|
||||
- For irreversible sigils: return the input unchanged (passthrough)
|
||||
|
||||
### 3.3 Transmute Function
|
||||
|
||||
The framework provides a helper function for applying multiple sigils:
|
||||
|
||||
```
|
||||
function Transmute(data: bytes, sigils: Sigil[]) -> (bytes, error):
|
||||
for each sigil in sigils:
|
||||
data, err = sigil.In(data)
|
||||
if err != nil:
|
||||
return nil, err
|
||||
return data, nil
|
||||
```
|
||||
|
||||
## 4. Sigil Categories
|
||||
|
||||
### 4.1 Reversible Sigils
|
||||
|
||||
Reversible sigils can recover the original input from the output.
|
||||
|
||||
**Property**: For any valid input `x`:
|
||||
```
|
||||
sigil.Out(sigil.In(x)) == x
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Encoding sigils (hex, base64)
|
||||
- Compression sigils (gzip)
|
||||
- Encryption sigils (ChaCha20-Poly1305)
|
||||
|
||||
### 4.2 Irreversible Sigils
|
||||
|
||||
Irreversible sigils perform one-way transformations.
|
||||
|
||||
**Property**: The `Out` method returns input unchanged:
|
||||
```
|
||||
sigil.Out(x) == x
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Hash sigils (SHA-256, MD5)
|
||||
- Truncation sigils
|
||||
|
||||
### 4.3 Symmetric Sigils
|
||||
|
||||
Symmetric sigils have identical `In` and `Out` operations.
|
||||
|
||||
**Property**: For any input `x`:
|
||||
```
|
||||
sigil.In(x) == sigil.Out(x)
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Byte reversal
|
||||
- XOR with fixed key
|
||||
- Bitwise NOT
|
||||
|
||||
## 5. Standard Sigils
|
||||
|
||||
### 5.1 Encoding Sigils
|
||||
|
||||
#### 5.1.1 Hex Sigil
|
||||
|
||||
Encodes data to hexadecimal representation.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `hex` |
|
||||
| Category | Reversible |
|
||||
| In | Binary to hex ASCII |
|
||||
| Out | Hex ASCII to binary |
|
||||
| Output expansion | 2x |
|
||||
|
||||
```
|
||||
In("Hello") -> "48656c6c6f"
|
||||
Out("48656c6c6f") -> "Hello"
|
||||
```
|
||||
|
||||
#### 5.1.2 Base64 Sigil
|
||||
|
||||
Encodes data to Base64 representation (RFC 4648).
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `base64` |
|
||||
| Category | Reversible |
|
||||
| In | Binary to Base64 ASCII |
|
||||
| Out | Base64 ASCII to binary |
|
||||
| Output expansion | ~1.33x |
|
||||
|
||||
```
|
||||
In("Hello") -> "SGVsbG8="
|
||||
Out("SGVsbG8=") -> "Hello"
|
||||
```
|
||||
|
||||
### 5.2 Transformation Sigils
|
||||
|
||||
#### 5.2.1 Reverse Sigil
|
||||
|
||||
Reverses the byte order of the data.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `reverse` |
|
||||
| Category | Symmetric |
|
||||
| In | Reverse bytes |
|
||||
| Out | Reverse bytes |
|
||||
| Output expansion | 1x |
|
||||
|
||||
```
|
||||
In("Hello") -> "olleH"
|
||||
Out("olleH") -> "Hello"
|
||||
```
|
||||
|
||||
### 5.3 Compression Sigils
|
||||
|
||||
#### 5.3.1 Gzip Sigil
|
||||
|
||||
Compresses data using gzip (RFC 1952).
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `gzip` |
|
||||
| Category | Reversible |
|
||||
| In | Compress |
|
||||
| Out | Decompress |
|
||||
| Output expansion | Variable (typically < 1x) |
|
||||
|
||||
### 5.4 Formatting Sigils
|
||||
|
||||
#### 5.4.1 JSON Sigil
|
||||
|
||||
Compacts JSON data by removing whitespace.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `json` |
|
||||
| Category | Reversible* |
|
||||
| In | Compact JSON |
|
||||
| Out | Passthrough |
|
||||
|
||||
*Note: Whitespace is not recoverable; Out returns input unchanged.
|
||||
|
||||
#### 5.4.2 JSON-Indent Sigil
|
||||
|
||||
Pretty-prints JSON data with indentation.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `json-indent` |
|
||||
| Category | Reversible* |
|
||||
| In | Indent JSON (2 spaces) |
|
||||
| Out | Passthrough |
|
||||
|
||||
### 5.5 Hash Sigils
|
||||
|
||||
Hash sigils compute cryptographic digests. They are irreversible.
|
||||
|
||||
| Name | Algorithm | Output Size |
|
||||
|------|-----------|-------------|
|
||||
| `md4` | MD4 | 16 bytes |
|
||||
| `md5` | MD5 | 16 bytes |
|
||||
| `sha1` | SHA-1 | 20 bytes |
|
||||
| `sha224` | SHA-224 | 28 bytes |
|
||||
| `sha256` | SHA-256 | 32 bytes |
|
||||
| `sha384` | SHA-384 | 48 bytes |
|
||||
| `sha512` | SHA-512 | 64 bytes |
|
||||
| `sha3-224` | SHA3-224 | 28 bytes |
|
||||
| `sha3-256` | SHA3-256 | 32 bytes |
|
||||
| `sha3-384` | SHA3-384 | 48 bytes |
|
||||
| `sha3-512` | SHA3-512 | 64 bytes |
|
||||
| `sha512-224` | SHA-512/224 | 28 bytes |
|
||||
| `sha512-256` | SHA-512/256 | 32 bytes |
|
||||
| `ripemd160` | RIPEMD-160 | 20 bytes |
|
||||
| `blake2s-256` | BLAKE2s | 32 bytes |
|
||||
| `blake2b-256` | BLAKE2b | 32 bytes |
|
||||
| `blake2b-384` | BLAKE2b | 48 bytes |
|
||||
| `blake2b-512` | BLAKE2b | 64 bytes |
|
||||
|
||||
For all hash sigils:
|
||||
- `In(data)` returns the hash digest as raw bytes
|
||||
- `Out(data)` returns data unchanged (passthrough)
|
||||
|
||||
## 6. Composition and Chaining
|
||||
|
||||
### 6.1 Forward Chain (Packing)
|
||||
|
||||
Sigils are applied left-to-right:
|
||||
|
||||
```
|
||||
sigils = [gzip, base64, hex]
|
||||
result = Transmute(data, sigils)
|
||||
|
||||
// Equivalent to:
|
||||
result = hex.In(base64.In(gzip.In(data)))
|
||||
```
|
||||
|
||||
### 6.2 Reverse Chain (Unpacking)
|
||||
|
||||
To reverse a chain, apply `Out` in reverse order:
|
||||
|
||||
```
|
||||
function ReverseTransmute(data: bytes, sigils: Sigil[]) -> (bytes, error):
|
||||
for i = length(sigils) - 1 downto 0:
|
||||
data, err = sigils[i].Out(data)
|
||||
if err != nil:
|
||||
return nil, err
|
||||
return data, nil
|
||||
```
|
||||
|
||||
### 6.3 Chain Properties
|
||||
|
||||
For a chain of reversible sigils `[s1, s2, s3]`:
|
||||
|
||||
```
|
||||
original = ReverseTransmute(Transmute(data, [s1, s2, s3]), [s1, s2, s3])
|
||||
// original == data
|
||||
```
|
||||
|
||||
### 6.4 Mixed Chains
|
||||
|
||||
Chains MAY contain both reversible and irreversible sigils:
|
||||
|
||||
```
|
||||
sigils = [gzip, sha256] // sha256 is irreversible
|
||||
|
||||
packed = Transmute(data, sigils)
|
||||
// packed is the SHA-256 hash of gzip-compressed data
|
||||
|
||||
unpacked = ReverseTransmute(packed, sigils)
|
||||
// unpacked == packed (sha256.Out is passthrough)
|
||||
```
|
||||
|
||||
## 7. Error Handling
|
||||
|
||||
### 7.1 Error Categories
|
||||
|
||||
| Category | Description | Recovery |
|
||||
|----------|-------------|----------|
|
||||
| Input error | Invalid input format | Check input validity |
|
||||
| State error | Sigil not properly configured | Initialize sigil |
|
||||
| Resource error | Memory/IO failure | Retry or abort |
|
||||
| Algorithm error | Cryptographic failure | Check keys/params |
|
||||
|
||||
### 7.2 Error Propagation
|
||||
|
||||
Errors MUST propagate immediately:
|
||||
|
||||
```
|
||||
function Transmute(data: bytes, sigils: Sigil[]) -> (bytes, error):
|
||||
for each sigil in sigils:
|
||||
data, err = sigil.In(data)
|
||||
if err != nil:
|
||||
return nil, err // Stop immediately
|
||||
return data, nil
|
||||
```
|
||||
|
||||
### 7.3 Partial Results
|
||||
|
||||
On error, implementations MUST NOT return partial results. Either:
|
||||
- Return complete transformed data, or
|
||||
- Return nil with an error
|
||||
|
||||
## 8. Implementation Guidelines
|
||||
|
||||
### 8.1 Sigil Factory
|
||||
|
||||
Implementations SHOULD provide a factory function:
|
||||
|
||||
```
|
||||
function NewSigil(name: string) -> (Sigil, error):
|
||||
switch name:
|
||||
case "hex": return new HexSigil()
|
||||
case "base64": return new Base64Sigil()
|
||||
case "gzip": return new GzipSigil()
|
||||
// ... etc
|
||||
default: return nil, error("unknown sigil: " + name)
|
||||
```
|
||||
|
||||
### 8.2 Null Safety
|
||||
|
||||
```
|
||||
function In(data: bytes) -> (bytes, error):
|
||||
if data == nil:
|
||||
return nil, nil // NOT an error
|
||||
if length(data) == 0:
|
||||
return [], nil // Empty slice, NOT nil
|
||||
// ... perform transformation
|
||||
```
|
||||
|
||||
### 8.3 Immutability
|
||||
|
||||
Sigils SHOULD NOT modify the input slice:
|
||||
|
||||
```
|
||||
// CORRECT: Create new slice
|
||||
result := make([]byte, len(data))
|
||||
// ... transform into result
|
||||
|
||||
// INCORRECT: Modify in place
|
||||
data[0] = transformed // Don't do this
|
||||
```
|
||||
|
||||
### 8.4 Thread Safety
|
||||
|
||||
Sigils SHOULD be safe for concurrent use:
|
||||
|
||||
- Avoid mutable state in sigil instances
|
||||
- Use synchronization if state is required
|
||||
- Document thread-safety guarantees
|
||||
|
||||
## 9. Security Considerations
|
||||
|
||||
### 9.1 Hash Sigil Security
|
||||
|
||||
- MD4, MD5, SHA1 are cryptographically broken for collision resistance
|
||||
- Use SHA-256 or stronger for security-critical applications
|
||||
- Hash sigils do NOT provide authentication
|
||||
|
||||
### 9.2 Compression Oracle Attacks
|
||||
|
||||
When combining compression and encryption sigils:
|
||||
- Be aware of CRIME/BREACH-style attacks
|
||||
- Do not compress data containing secrets alongside attacker-controlled data
|
||||
|
||||
### 9.3 Memory Safety
|
||||
|
||||
- Validate output buffer sizes before allocation
|
||||
- Implement maximum input size limits
|
||||
- Handle decompression bombs (zip bombs)
|
||||
|
||||
### 9.4 Timing Attacks
|
||||
|
||||
- Comparison operations should be constant-time where security-relevant
|
||||
- Hash comparisons should use constant-time comparison functions
|
||||
|
||||
## 10. References
|
||||
|
||||
- [RFC 4648] The Base16, Base32, and Base64 Data Encodings
|
||||
- [RFC 1952] GZIP file format specification
|
||||
- [RFC 8259] The JavaScript Object Notation (JSON) Data Interchange Format
|
||||
- [FIPS 180-4] Secure Hash Standard
|
||||
- [FIPS 202] SHA-3 Standard
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Sigil Name Registry
|
||||
|
||||
| Name | Category | Reversible | Notes |
|
||||
|------|----------|------------|-------|
|
||||
| `reverse` | Transform | Yes (symmetric) | Byte reversal |
|
||||
| `hex` | Encoding | Yes | Hexadecimal |
|
||||
| `base64` | Encoding | Yes | RFC 4648 |
|
||||
| `gzip` | Compression | Yes | RFC 1952 |
|
||||
| `json` | Formatting | Partial | Compacts JSON |
|
||||
| `json-indent` | Formatting | Partial | Pretty-prints JSON |
|
||||
| `md4` | Hash | No | 128-bit |
|
||||
| `md5` | Hash | No | 128-bit |
|
||||
| `sha1` | Hash | No | 160-bit |
|
||||
| `sha224` | Hash | No | 224-bit |
|
||||
| `sha256` | Hash | No | 256-bit |
|
||||
| `sha384` | Hash | No | 384-bit |
|
||||
| `sha512` | Hash | No | 512-bit |
|
||||
| `sha3-*` | Hash | No | SHA-3 family |
|
||||
| `sha512-*` | Hash | No | SHA-512 truncated |
|
||||
| `ripemd160` | Hash | No | 160-bit |
|
||||
| `blake2s-256` | Hash | No | 256-bit |
|
||||
| `blake2b-*` | Hash | No | BLAKE2b family |
|
||||
|
||||
## Appendix B: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
- Interface: `github.com/Snider/Enchantrix/pkg/enchantrix/enchantrix.go`
|
||||
- Standard sigils: `github.com/Snider/Enchantrix/pkg/enchantrix/sigils.go`
|
||||
|
||||
## Appendix C: Custom Sigil Example
|
||||
|
||||
```go
|
||||
// ROT13Sigil implements a simple letter rotation cipher.
|
||||
type ROT13Sigil struct{}
|
||||
|
||||
func (s *ROT13Sigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
result := make([]byte, len(data))
|
||||
for i, b := range data {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
result[i] = 'A' + (b-'A'+13)%26
|
||||
} else if b >= 'a' && b <= 'z' {
|
||||
result[i] = 'a' + (b-'a'+13)%26
|
||||
} else {
|
||||
result[i] = b
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ROT13Sigil) Out(data []byte) ([]byte, error) {
|
||||
return s.In(data) // ROT13 is symmetric
|
||||
}
|
||||
```
|
||||
|
||||
## Appendix D: Changelog
|
||||
|
||||
- **1.0** (2025-01-13): Initial specification
|
||||
315
rfcs/RFC-0004-LTHN-Hash-Algorithm.md
Normal file
315
rfcs/RFC-0004-LTHN-Hash-Algorithm.md
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
# 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
## 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. References
|
||||
|
||||
- [FIPS 180-4] Secure Hash Standard (SHA-256)
|
||||
- [RFC 4648] The Base16, Base32, and Base64 Data Encodings
|
||||
- [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
|
||||
Loading…
Add table
Reference in a new issue