Compare commits
79 commits
feat-proxy
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86f4e33b1a | ||
|
|
bdef246a87 | ||
|
|
748ca6ddd7 | ||
|
|
1e1dfee01b | ||
|
|
afb11667e6 | ||
| e8a3fb3646 | |||
|
|
e7a736e128 | ||
| 835520f946 | |||
|
|
fce5b3fa59 | ||
| 120a9b9f2c | |||
|
|
fca2880355 | ||
| deff3a80c6 | |||
|
|
e112ec363d | ||
| d649e9e69e | |||
|
|
032c8fae93 | ||
|
|
16a346ca99 | ||
|
|
18ac6b99bc | ||
|
|
33e7fa1e17 | ||
|
|
8082074054 | ||
|
|
dd3eb4fedf | ||
|
|
91e7268143 | ||
|
|
b6b526bcf7 | ||
|
|
a46477c8fd | ||
| 248de1e9df | |||
|
|
4292d56caa | ||
|
|
b17d32999c | ||
|
|
695fe6dfeb | ||
|
|
6d9ae98916 | ||
|
|
6faf6d9822 | ||
|
|
c5de11834d | ||
| 68acd6b775 | |||
| b03ba0cd99 | |||
| 1f7fae72b1 | |||
|
|
209b2e395d | ||
|
|
ac706983ed | ||
| 3ab55c98fc | |||
|
|
47db6efff9 | ||
|
|
edb8b8f98e | ||
|
|
1a4b2923bf | ||
|
|
0ca908f434 | ||
|
|
85d3a237eb | ||
| 9be80618ae | |||
| 39d06c96ff | |||
|
|
3da7a0468b | ||
| 06720ce8dc | |||
|
|
e625b4b4e1 | ||
| 5f4682953b | |||
| 6c3b1069ce | |||
|
|
d7c738bbd3 | ||
| 0f8917e578 | |||
|
|
4f83430aa4 | ||
| 0e04f21686 | |||
|
|
b13694bcc2 | ||
|
|
1a114d1f64 | ||
|
|
c286a82e89 | ||
|
|
8e9a7d71fa | ||
|
|
b4ef069ee6 | ||
|
|
aca835874a | ||
|
|
0e50aee481 | ||
|
|
8cf1df9495 | ||
|
|
56f28c1ea5 | ||
|
|
f54a7fc067 | ||
|
|
e9f0e9f43f | ||
|
|
af9a6076c4 | ||
|
|
234157b73a | ||
|
|
e7aeb3c8b8 | ||
|
|
11a2c85a33 | ||
|
|
f51ef1b52e | ||
|
|
3f39b81518 | ||
|
|
6168a9d7fe | ||
|
|
3ad62c3be3 | ||
|
|
60ce78a52c | ||
|
|
186b75c402 | ||
|
|
9dcb399988 | ||
|
|
83e8174634 | ||
|
|
52aa833a2f | ||
|
|
f88d37cb4a | ||
|
|
e48e38419b | ||
|
|
d66acec498 |
70 changed files with 7892 additions and 877 deletions
15
.github/workflows/go.yml
vendored
15
.github/workflows/go.yml
vendored
|
|
@ -23,9 +23,20 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v ./...
|
run: go build -v ./...
|
||||||
|
|
||||||
- name: Test
|
- name: Vet
|
||||||
run: go test -v -coverprofile=coverage.out ./...
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Test (race + coverage)
|
||||||
|
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||||
|
|
||||||
|
- name: Fuzz (10s)
|
||||||
|
run: go test -run=Fuzz -fuzz=Fuzz -fuzztime=10s ./pkg/trix
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
17
.github/workflows/mkdocs.yml
vendored
Normal file
17
.github/workflows/mkdocs.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
name: Publish Docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- run: |
|
||||||
|
pip install mkdocs-material
|
||||||
|
- run: |
|
||||||
|
cd docs && mkdocs gh-deploy --force --clean --verbose
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -1,5 +1,14 @@
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.idea
|
.idea
|
||||||
go.sum
|
covdata/
|
||||||
miner
|
merged_covdata/
|
||||||
|
coverage.txt
|
||||||
|
coverage.html
|
||||||
|
coverage.out
|
||||||
|
test.*
|
||||||
|
# Build artifacts
|
||||||
|
/dist/
|
||||||
|
/site/
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
35
.goreleaser.yml
Normal file
35
.goreleaser.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# .goreleaser.yml
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
main: ./cmd/trix
|
||||||
|
binary: trix
|
||||||
|
id: "trix"
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- replacements:
|
||||||
|
darwin: Darwin
|
||||||
|
linux: Linux
|
||||||
|
windows: Windows
|
||||||
|
386: i386
|
||||||
|
amd64: x86_64
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
85
CLAUDE.md
Normal file
85
CLAUDE.md
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build and Test Commands
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Run all tests with coverage
|
||||||
|
go test -v -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
go test -v -run TestName ./pkg/enchantrix
|
||||||
|
|
||||||
|
# Run tests with race detection (as CI does)
|
||||||
|
go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||||
|
|
||||||
|
# Run fuzz tests (CI runs 10s)
|
||||||
|
go test -run=Fuzz -fuzz=Fuzz -fuzztime=10s ./pkg/trix
|
||||||
|
|
||||||
|
# Build
|
||||||
|
go build -v ./...
|
||||||
|
|
||||||
|
# Vet
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
# Format
|
||||||
|
go fmt ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
If Task is installed, these are available:
|
||||||
|
- `task test` - Run tests with coverage
|
||||||
|
- `task build` - Build project
|
||||||
|
- `task fmt` - Format code
|
||||||
|
- `task vet` - Run go vet
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Enchantrix is an encryption library with a custom `.trix` file format and CLI tool.
|
||||||
|
|
||||||
|
### Core Packages
|
||||||
|
|
||||||
|
**pkg/enchantrix** - Core transformation framework
|
||||||
|
- `Sigil` interface: defines `In(data)` and `Out(data)` for reversible/irreversible transforms
|
||||||
|
- `Transmute()`: applies a chain of sigils to data
|
||||||
|
- Built-in sigils: `reverse`, `hex`, `base64`, `gzip`, `json`, `json-indent`
|
||||||
|
- Hash sigils: `md4`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `ripemd160`, `sha3-*`, `sha512-*`, `blake2s-256`, `blake2b-*`
|
||||||
|
- `NewSigil(name)`: factory function to create sigils by string name
|
||||||
|
- `ChaChaPolySigil`: encryption sigil using XChaCha20-Poly1305 with pre-obfuscation layer
|
||||||
|
|
||||||
|
**pkg/trix** - Binary file format (.trix)
|
||||||
|
- Format: `[4-byte magic][1-byte version][4-byte header len][JSON header][payload]`
|
||||||
|
- `Encode()`: serializes Trix struct to binary
|
||||||
|
- `Decode()`: deserializes binary to Trix struct
|
||||||
|
- `Pack()`/`Unpack()`: apply/reverse sigils on payload
|
||||||
|
- Supports optional checksums via `ChecksumAlgo` field
|
||||||
|
|
||||||
|
**pkg/crypt** - Cryptographic services facade
|
||||||
|
- `Service`: aggregates hashing, checksums, RSA, and PGP operations
|
||||||
|
- Hash types: `lthn` (custom), `sha512`, `sha256`, `sha1`, `md5`
|
||||||
|
- Checksums: `Luhn()`, `Fletcher16/32/64()`
|
||||||
|
- RSA: key generation, encrypt/decrypt via `pkg/crypt/std/rsa`
|
||||||
|
- PGP: key generation, encrypt/decrypt, sign/verify, symmetric encrypt via `pkg/crypt/std/pgp`
|
||||||
|
|
||||||
|
**cmd/trix** - CLI tool (Cobra-based)
|
||||||
|
- `trix encode --magic XXXX --output file [sigils...]`
|
||||||
|
- `trix decode --magic XXXX --output file [sigils...]`
|
||||||
|
- `trix hash [algorithm]`
|
||||||
|
- `trix [sigil]` - apply any sigil directly
|
||||||
|
|
||||||
|
### Key Design Patterns
|
||||||
|
|
||||||
|
1. **Sigil Chain**: Transformations are composable. Encoding chains sigils in order; decoding reverses.
|
||||||
|
2. **Pre-Obfuscation**: `ChaChaPolySigil` applies XOR or shuffle-mask obfuscation before encryption so raw plaintext never goes directly to CPU encryption routines.
|
||||||
|
3. **Streaming Support**: `Encode()`/`Decode()` accept optional `io.Writer`/`io.Reader` for streaming.
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
- Tests use `testify/assert` and `testify/require`
|
||||||
|
- Test files follow `*_test.go` pattern adjacent to implementation
|
||||||
|
- `examples_test.go` files contain example functions for godoc
|
||||||
|
- Fuzz tests exist in `pkg/trix` (`go test -fuzz`)
|
||||||
|
|
||||||
|
## Go Version
|
||||||
|
|
||||||
|
Minimum Go 1.25. Uses `go.work` for workspace management.
|
||||||
214
README.md
214
README.md
|
|
@ -1,15 +1,217 @@
|
||||||
# Enchantrix
|
# Enchantrix
|
||||||
|
|
||||||
Enchantrix is a Go-based encryption library for the Core framework, 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.
|
[](https://goreportcard.com/report/github.com/Snider/Enchantrix)
|
||||||
|
[](https://godoc.org/github.com/Snider/Enchantrix)
|
||||||
|
[](https://github.com/Snider/Enchantrix/actions/workflows/go.yml)
|
||||||
|
[](https://codecov.io/github/Snider/Enchantrix)
|
||||||
|
[](https://github.com/Snider/Enchantrix/releases/latest)
|
||||||
|
[](https://github.com/Snider/Enchantrix/blob/main/LICENCE)
|
||||||
|
[](https://go.dev/)
|
||||||
|
|
||||||
## Test-Driven Development
|
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.
|
||||||
|
|
||||||
This project follows a strict Test-Driven Development (TDD) methodology. All new functionality must be accompanied by a comprehensive suite of tests.
|
## Features
|
||||||
|
|
||||||
## Getting Started
|
- **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
|
||||||
|
|
||||||
To get started with Enchantrix, you'll need to have Go installed. You can then run the tests using the following command:
|
## Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
go test ./...
|
go get github.com/Snider/Enchantrix
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install CLI Tool
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go install github.com/Snider/Enchantrix/cmd/trix@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
#### Sigil Transformations
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Hashing
|
||||||
|
|
||||||
|
```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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Encrypted .trix Container
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Examples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Specifications
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available via MkDocs:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Install dependencies
|
||||||
|
pip install mkdocs mkdocs-material
|
||||||
|
|
||||||
|
# Serve locally
|
||||||
|
mkdocs serve -a 127.0.0.1:8000
|
||||||
|
|
||||||
|
# Build static site
|
||||||
|
mkdocs build --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Go 1.25 or later
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 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
|
||||||
|
|
||||||
|
Built with GoReleaser:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Snapshot release (local, no publish)
|
||||||
|
goreleaser release --snapshot --clean
|
||||||
|
|
||||||
|
# Production release (requires Git tag)
|
||||||
|
goreleaser release --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [LICENCE](LICENCE) for details.
|
||||||
|
|
|
||||||
35
Taskfile.yml
35
Taskfile.yml
|
|
@ -2,11 +2,42 @@ version: '3'
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
test:
|
test:
|
||||||
desc: "Run all tests"
|
desc: "Run all tests and generate a coverage report"
|
||||||
cmds:
|
cmds:
|
||||||
- go test -v ./...
|
- go vet ./...
|
||||||
|
- go test -v -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
build:
|
build:
|
||||||
desc: "Build the project"
|
desc: "Build the project"
|
||||||
cmds:
|
cmds:
|
||||||
- go build -v ./...
|
- go build -v ./...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
desc: "Format the code"
|
||||||
|
cmds:
|
||||||
|
- go fmt ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
desc: "Run go vet"
|
||||||
|
cmds:
|
||||||
|
- go vet ./...
|
||||||
|
|
||||||
|
docs:serve:
|
||||||
|
desc: "Serve the MkDocs site locally"
|
||||||
|
cmds:
|
||||||
|
- mkdocs serve -a 127.0.0.1:8000
|
||||||
|
|
||||||
|
docs:build:
|
||||||
|
desc: "Build the MkDocs site"
|
||||||
|
cmds:
|
||||||
|
- mkdocs build --strict
|
||||||
|
|
||||||
|
release:snapshot:
|
||||||
|
desc: "Create a snapshot release with GoReleaser (no publishing)"
|
||||||
|
cmds:
|
||||||
|
- goreleaser release --snapshot --clean
|
||||||
|
|
||||||
|
release:
|
||||||
|
desc: "Create a release with GoReleaser"
|
||||||
|
cmds:
|
||||||
|
- goreleaser release --clean
|
||||||
|
|
|
||||||
184
cmd/mine/main.go
184
cmd/mine/main.go
|
|
@ -1,184 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/Snider/Enchantrix/pkg/config"
|
|
||||||
"github.com/Snider/Enchantrix/pkg/miner"
|
|
||||||
"github.com/Snider/Enchantrix/pkg/pool"
|
|
||||||
"github.com/Snider/Enchantrix/pkg/proxy"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/leaanthony/clir"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Create a new cli application
|
|
||||||
cli := clir.NewCli("Enchantrix Miner", "A miner for the Enchantrix project", "v0.0.1")
|
|
||||||
|
|
||||||
// Create a new config
|
|
||||||
cfg := config.New()
|
|
||||||
|
|
||||||
// Create a start command
|
|
||||||
startCmd := cli.NewSubCommand("start", "Starts the miner")
|
|
||||||
|
|
||||||
// Define flags
|
|
||||||
var configFile string
|
|
||||||
startCmd.StringFlag("config", "Path to config file", &configFile)
|
|
||||||
|
|
||||||
var logLevel string
|
|
||||||
startCmd.StringFlag("log-level", "Log level (trace, debug, info, warn, error, fatal, panic)", &logLevel)
|
|
||||||
|
|
||||||
var url string
|
|
||||||
startCmd.StringFlag("url", "URL of mining pool", &url)
|
|
||||||
|
|
||||||
var user string
|
|
||||||
startCmd.StringFlag("user", "Username for mining pool", &user)
|
|
||||||
|
|
||||||
var pass string
|
|
||||||
startCmd.StringFlag("pass", "Password for mining pool", &pass)
|
|
||||||
|
|
||||||
var numThreads int
|
|
||||||
startCmd.IntFlag("threads", "Number of miner threads", &numThreads)
|
|
||||||
|
|
||||||
|
|
||||||
startCmd.Action(func() error {
|
|
||||||
// Set up logging
|
|
||||||
level, err := logrus.ParseLevel(logLevel)
|
|
||||||
if err != nil {
|
|
||||||
level = logrus.InfoLevel
|
|
||||||
}
|
|
||||||
logrus.SetLevel(level)
|
|
||||||
|
|
||||||
// Load config from file if specified
|
|
||||||
if configFile != "" {
|
|
||||||
if err := cfg.Load(configFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("Starting the miner...")
|
|
||||||
|
|
||||||
// Override config with flags
|
|
||||||
if url != "" {
|
|
||||||
cfg.Pools = []struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
User string `json:"user"`
|
|
||||||
Pass string `json:"pass"`
|
|
||||||
}{{URL: url, User: user, Pass: pass}}
|
|
||||||
}
|
|
||||||
if numThreads == 0 {
|
|
||||||
numThreads = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Create a new miner
|
|
||||||
algo := &miner.MockAlgo{}
|
|
||||||
m := miner.New(algo, cfg.Pools[0].URL, cfg.Pools[0].User, cfg.Pools[0].Pass, numThreads)
|
|
||||||
m.Start()
|
|
||||||
defer m.Stop()
|
|
||||||
|
|
||||||
// Create a new pool client
|
|
||||||
p := pool.New(cfg.Pools[0].URL, cfg.Pools[0].User, cfg.Pools[0].Pass, m.JobQueue)
|
|
||||||
p.Start()
|
|
||||||
defer p.Stop()
|
|
||||||
|
|
||||||
|
|
||||||
if cfg.Pools[0].URL != "" {
|
|
||||||
logrus.Infof("Connecting to %s as %s", cfg.Pools[0].URL, cfg.Pools[0].User)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the Gin router
|
|
||||||
router := gin.Default()
|
|
||||||
router.GET("/1/miners", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, []gin.H{
|
|
||||||
{
|
|
||||||
"id": 0,
|
|
||||||
"status": "running",
|
|
||||||
"summary": m.StateManager.Summary(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
router.GET("/1/miner/:id/status", func(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil || id != 0 {
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "miner not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, m.StateManager.Summary())
|
|
||||||
})
|
|
||||||
router.GET("/1/config", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, cfg.Get())
|
|
||||||
})
|
|
||||||
router.PUT("/1/config", func(c *gin.Context) {
|
|
||||||
var newConfig config.Config
|
|
||||||
if err := c.ShouldBindJSON(&newConfig); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfg.Update(&newConfig)
|
|
||||||
c.JSON(http.StatusOK, cfg.Get())
|
|
||||||
})
|
|
||||||
router.GET("/1/threads", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, m.StateManager.ThreadsSummary())
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
logrus.Infof("Starting API server on http://%s:%d", cfg.HTTP.Host, cfg.HTTP.Port)
|
|
||||||
return router.Run(fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create a proxy command
|
|
||||||
proxyCmd := cli.NewSubCommand("proxy", "Starts the proxy")
|
|
||||||
|
|
||||||
// Define flags
|
|
||||||
var proxyConfigFile string
|
|
||||||
proxyCmd.StringFlag("config", "Path to config file", &proxyConfigFile)
|
|
||||||
|
|
||||||
var proxyLogLevel string
|
|
||||||
proxyCmd.StringFlag("log-level", "Log level (trace, debug, info, warn, error, fatal, panic)", &proxyLogLevel)
|
|
||||||
|
|
||||||
proxyCmd.Action(func() error {
|
|
||||||
// Set up logging
|
|
||||||
level, err := logrus.ParseLevel(proxyLogLevel)
|
|
||||||
if err != nil {
|
|
||||||
level = logrus.InfoLevel
|
|
||||||
}
|
|
||||||
logrus.SetLevel(level)
|
|
||||||
|
|
||||||
// Load config from file if specified
|
|
||||||
if proxyConfigFile != "" {
|
|
||||||
if err := cfg.Load(proxyConfigFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("Starting the proxy...")
|
|
||||||
|
|
||||||
// Create a new proxy
|
|
||||||
p := proxy.New()
|
|
||||||
p.Start()
|
|
||||||
defer p.Stop()
|
|
||||||
|
|
||||||
// Set up the Gin router
|
|
||||||
router := gin.Default()
|
|
||||||
router.GET("/", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, p.Summary())
|
|
||||||
})
|
|
||||||
router.GET("/workers.json", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, p.WorkersSummary())
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
logrus.Infof("Starting API server on http://%s:%d", cfg.HTTP.Host, cfg.HTTP.Port)
|
|
||||||
return router.Run(fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port))
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// Run the cli
|
|
||||||
if err := cli.Run(); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
227
cmd/trix/main.go
Normal file
227
cmd/trix/main.go
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/trix"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "trix",
|
||||||
|
Short: "A tool for encoding and decoding .trix files",
|
||||||
|
Long: `trix is a command-line tool for working with the .trix file format, which is used for storing encrypted data.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeCmd = &cobra.Command{
|
||||||
|
Use: "encode",
|
||||||
|
Short: "Encode a file to the .trix format",
|
||||||
|
RunE: runEncode,
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeCmd = &cobra.Command{
|
||||||
|
Use: "decode",
|
||||||
|
Short: "Decode a .trix file",
|
||||||
|
RunE: runDecode,
|
||||||
|
}
|
||||||
|
|
||||||
|
hashCmd = &cobra.Command{
|
||||||
|
Use: "hash [algorithm]",
|
||||||
|
Short: "Hash a file using a specified algorithm",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runHash,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var availableSigils = []string{
|
||||||
|
"reverse", "hex", "base64", "gzip", "json", "json-indent", "md4", "md5",
|
||||||
|
"sha1", "sha224", "sha256", "sha384", "sha512", "ripemd160", "sha3-224",
|
||||||
|
"sha3-256", "sha3-384", "sha3-512", "sha512-224", "sha512-256",
|
||||||
|
"blake2s-256", "blake2b-256", "blake2b-384", "blake2b-512",
|
||||||
|
}
|
||||||
|
|
||||||
|
var exit = os.Exit
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Add flags to encode command
|
||||||
|
encodeCmd.Flags().StringP("input", "i", "", "Input file (or stdin)")
|
||||||
|
encodeCmd.Flags().StringP("output", "o", "", "Output file")
|
||||||
|
encodeCmd.Flags().StringP("magic", "m", "", "Magic number (4 bytes)")
|
||||||
|
|
||||||
|
// Add flags to decode command
|
||||||
|
decodeCmd.Flags().StringP("input", "i", "", "Input file (or stdin)")
|
||||||
|
decodeCmd.Flags().StringP("output", "o", "", "Output file")
|
||||||
|
decodeCmd.Flags().StringP("magic", "m", "", "Magic number (4 bytes)")
|
||||||
|
|
||||||
|
// Add flags to hash command
|
||||||
|
hashCmd.Flags().StringP("input", "i", "", "Input file (or stdin)")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(encodeCmd, decodeCmd, hashCmd)
|
||||||
|
|
||||||
|
// Add sigil commands
|
||||||
|
for _, sigilName := range availableSigils {
|
||||||
|
sigilCmd := &cobra.Command{
|
||||||
|
Use: sigilName,
|
||||||
|
Short: "Apply the " + sigilName + " sigil",
|
||||||
|
RunE: createSigilRunE(sigilName),
|
||||||
|
}
|
||||||
|
sigilCmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
|
||||||
|
rootCmd.AddCommand(sigilCmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSigilRunE(sigilName string) func(cmd *cobra.Command, args []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
input, _ := cmd.Flags().GetString("input")
|
||||||
|
return handleSigil(cmd, sigilName, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runEncode(cmd *cobra.Command, args []string) error {
|
||||||
|
input, _ := cmd.Flags().GetString("input")
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
magic, _ := cmd.Flags().GetString("magic")
|
||||||
|
return handleEncode(cmd, input, output, magic, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDecode(cmd *cobra.Command, args []string) error {
|
||||||
|
input, _ := cmd.Flags().GetString("input")
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
magic, _ := cmd.Flags().GetString("magic")
|
||||||
|
return handleDecode(cmd, input, output, magic, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHash(cmd *cobra.Command, args []string) error {
|
||||||
|
input, _ := cmd.Flags().GetString("input")
|
||||||
|
return handleHash(cmd, input, args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSigil(cmd *cobra.Command, sigilName, input string) error {
|
||||||
|
s, err := enchantrix.NewSigil(sigilName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
if input == "-" {
|
||||||
|
data, err = ioutil.ReadAll(cmd.InOrStdin())
|
||||||
|
} else if _, err := os.Stat(input); err == nil {
|
||||||
|
data, err = ioutil.ReadFile(input)
|
||||||
|
} else {
|
||||||
|
data = []byte(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := s.In(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.OutOrStdout().Write(out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHash(cmd *cobra.Command, inputFile, algo string) error {
|
||||||
|
if algo == "" {
|
||||||
|
return fmt.Errorf("hash algorithm is required")
|
||||||
|
}
|
||||||
|
service := crypt.NewService()
|
||||||
|
if !service.IsHashAlgo(algo) {
|
||||||
|
return fmt.Errorf("invalid hash algorithm: %s", algo)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if inputFile == "" || inputFile == "-" {
|
||||||
|
data, err = ioutil.ReadAll(cmd.InOrStdin())
|
||||||
|
} else {
|
||||||
|
data, err = ioutil.ReadFile(inputFile)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := service.Hash(crypt.HashType(algo), string(data))
|
||||||
|
cmd.OutOrStdout().Write([]byte(hash))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEncode(cmd *cobra.Command, inputFile, outputFile, magicNumber string, sigils []string) error {
|
||||||
|
if len(magicNumber) != 4 {
|
||||||
|
return fmt.Errorf("magic number must be 4 bytes long")
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if inputFile == "" || inputFile == "-" {
|
||||||
|
data, err = ioutil.ReadAll(cmd.InOrStdin())
|
||||||
|
} else {
|
||||||
|
data, err = ioutil.ReadFile(inputFile)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &trix.Trix{
|
||||||
|
Header: make(map[string]interface{}),
|
||||||
|
Payload: data,
|
||||||
|
InSigils: sigils,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.Pack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := trix.Encode(t, magicNumber, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputFile == "" || outputFile == "-" {
|
||||||
|
_, err = cmd.OutOrStdout().Write(encoded)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(outputFile, encoded, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDecode(cmd *cobra.Command, inputFile, outputFile, magicNumber string, sigils []string) error {
|
||||||
|
if len(magicNumber) != 4 {
|
||||||
|
return fmt.Errorf("magic number must be 4 bytes long")
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if inputFile == "" || inputFile == "-" {
|
||||||
|
data, err = ioutil.ReadAll(cmd.InOrStdin())
|
||||||
|
} else {
|
||||||
|
data, err = ioutil.ReadFile(inputFile)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := trix.Decode(data, magicNumber, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.OutSigils = sigils
|
||||||
|
if err := t.Unpack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputFile == "" || outputFile == "-" {
|
||||||
|
_, err = cmd.OutOrStdout().Write(t.Payload)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(outputFile, t.Payload, 0644)
|
||||||
|
}
|
||||||
149
cmd/trix/main_test.go
Normal file
149
cmd/trix/main_test.go
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain_Good(t *testing.T) {
|
||||||
|
// Redirect stdout to a buffer
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
// Run the main function
|
||||||
|
main()
|
||||||
|
|
||||||
|
// Restore stdout
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
// Read the output from the buffer
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
|
||||||
|
// Check that the output contains the help message
|
||||||
|
assert.Contains(t, buf.String(), "Usage:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain_Bad(t *testing.T) {
|
||||||
|
oldExit := exit
|
||||||
|
defer func() { exit = oldExit }()
|
||||||
|
var exitCode int
|
||||||
|
exit = func(code int) {
|
||||||
|
exitCode = code
|
||||||
|
}
|
||||||
|
rootCmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return errors.New("test error")
|
||||||
|
}
|
||||||
|
// The rootCmd needs to be reset so that the test can be run again
|
||||||
|
defer func() { rootCmd = &cobra.Command{
|
||||||
|
Use: "trix",
|
||||||
|
Short: "A tool for encoding and decoding .trix files",
|
||||||
|
Long: `trix is a command-line tool for working with the .trix file format, which is used for storing encrypted data.`,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
main()
|
||||||
|
assert.Equal(t, 1, exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleSigil_Good(t *testing.T) {
|
||||||
|
// Create a dummy command
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
|
||||||
|
// Run the handleSigil function
|
||||||
|
err := handleSigil(cmd, "base64", "hello")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the output is the base64 encoded string
|
||||||
|
assert.Equal(t, "aGVsbG8=", strings.TrimSpace(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleSigil_Bad(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
err := handleSigil(cmd, "bad-sigil", "hello")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEncodeAndDecode_Good(t *testing.T) {
|
||||||
|
// Encode
|
||||||
|
encodeCmd := &cobra.Command{}
|
||||||
|
encodeBuf := new(bytes.Buffer)
|
||||||
|
encodeCmd.SetOut(encodeBuf)
|
||||||
|
encodeCmd.SetIn(strings.NewReader("hello"))
|
||||||
|
encodeCmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
|
||||||
|
encodeCmd.Flags().StringP("output", "o", "-", "Output file")
|
||||||
|
encodeCmd.Flags().StringP("magic", "m", "TEST", "Magic number (4 bytes)")
|
||||||
|
err := runEncode(encodeCmd, []string{"base64"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, encodeBuf.String())
|
||||||
|
|
||||||
|
// Decode
|
||||||
|
decodeCmd := &cobra.Command{}
|
||||||
|
decodeBuf := new(bytes.Buffer)
|
||||||
|
decodeCmd.SetOut(decodeBuf)
|
||||||
|
decodeCmd.SetIn(encodeBuf) // Use the output of the encode as the input for the decode
|
||||||
|
decodeCmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
|
||||||
|
decodeCmd.Flags().StringP("output", "o", "-", "Output file")
|
||||||
|
decodeCmd.Flags().StringP("magic", "m", "TEST", "Magic number (4 bytes)")
|
||||||
|
err = runDecode(decodeCmd, []string{"base64"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", strings.TrimSpace(decodeBuf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEncode_Bad(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.Flags().StringP("magic", "m", "bad", "Magic number (4 bytes)")
|
||||||
|
err := runEncode(cmd, []string{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunDecode_Bad(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.Flags().StringP("magic", "m", "bad", "Magic number (4 bytes)")
|
||||||
|
err := runDecode(cmd, []string{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHash_Good(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
cmd.SetIn(strings.NewReader("hello"))
|
||||||
|
cmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
|
||||||
|
|
||||||
|
// Run the runHash function
|
||||||
|
err := runHash(cmd, []string{"sha256"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the output is not empty
|
||||||
|
assert.NotEmpty(t, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHash_Bad(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
err := runHash(cmd, []string{"bad-hash"})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSigilRunE_Good(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
cmd.SetIn(strings.NewReader("hello"))
|
||||||
|
cmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
|
||||||
|
|
||||||
|
// Run the createSigilRunE function
|
||||||
|
runE := createSigilRunE("base64")
|
||||||
|
err := runE(cmd, []string{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
20
config.json
20
config.json
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"api": {
|
|
||||||
"id": "enchantrix-from-file",
|
|
||||||
"worker-id": "worker-1"
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabled": true,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8081,
|
|
||||||
"access-token": null,
|
|
||||||
"restricted": true
|
|
||||||
},
|
|
||||||
"pools": [
|
|
||||||
{
|
|
||||||
"url": "pool.example.com:3333",
|
|
||||||
"user": "testuser",
|
|
||||||
"pass": "testpass"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
33
docs/checksums.md
Normal file
33
docs/checksums.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Checksums
|
||||||
|
|
||||||
|
This example demonstrates how to use the `crypt` service to calculate checksums using various algorithms.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func demoChecksums() {
|
||||||
|
fmt.Println("--- Checksum Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// Luhn
|
||||||
|
luhnPayloadGood := "49927398716"
|
||||||
|
luhnPayloadBad := "49927398717"
|
||||||
|
fmt.Printf("Luhn Checksum:\n")
|
||||||
|
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadGood, cryptService.Luhn(luhnPayloadGood))
|
||||||
|
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadBad, cryptService.Luhn(luhnPayloadBad))
|
||||||
|
|
||||||
|
// Fletcher
|
||||||
|
fletcherPayload := "abcde"
|
||||||
|
fmt.Printf("\nFletcher Checksums (Payload: \"%s\"):\n", fletcherPayload)
|
||||||
|
fmt.Printf(" - Fletcher16: %d\n", cryptService.Fletcher16(fletcherPayload))
|
||||||
|
fmt.Printf(" - Fletcher32: %d\n", cryptService.Fletcher32(fletcherPayload))
|
||||||
|
fmt.Printf(" - Fletcher64: %d\n", cryptService.Fletcher64(fletcherPayload))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
```
|
||||||
116
docs/cli.md
Normal file
116
docs/cli.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# CLI Reference
|
||||||
|
|
||||||
|
The `trix` command-line tool allows you to work with `.trix` files, apply sigils, and perform hashing operations directly from the terminal.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trix [command]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Flags
|
||||||
|
|
||||||
|
* `--help`: Show help for command.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `encode`
|
||||||
|
|
||||||
|
Encodes data into the `.trix` file format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trix encode [flags] [sigils...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
|
||||||
|
* `-i, --input string`: Input file path. If not specified, reads from stdin.
|
||||||
|
* `-o, --output string`: Output file path. If not specified, writes to stdout.
|
||||||
|
* `-m, --magic string`: Custom 4-byte magic number (e.g., `TRIX`).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Encode a file, apply gzip and base64 sigils, and save to output.trix
|
||||||
|
trix encode -i data.json -o output.trix -m TRIX gzip base64
|
||||||
|
```
|
||||||
|
|
||||||
|
### `decode`
|
||||||
|
|
||||||
|
Decodes a `.trix` file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trix decode [flags] [sigils...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
|
||||||
|
* `-i, --input string`: Input file path. If not specified, reads from stdin.
|
||||||
|
* `-o, --output string`: Output file path. If not specified, writes to stdout.
|
||||||
|
* `-m, --magic string`: Custom 4-byte magic number.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Decode a file, reversing the base64 and gzip sigils implicitly if stored in header,
|
||||||
|
# or explicit sigils can be passed if needed for unpacking steps not in header (though unlikely for standard use).
|
||||||
|
# Typically:
|
||||||
|
trix decode -i output.trix -o restored.json -m TRIX
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hash`
|
||||||
|
|
||||||
|
Hashes input data using a specified algorithm.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trix hash [algorithm] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
* `algorithm`: The hash algorithm to use (e.g., `sha256`, `md5`, `lthn`).
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
|
||||||
|
* `-i, --input string`: Input file path. If not specified, reads from stdin.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "hello" | trix hash sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sigil Commands
|
||||||
|
|
||||||
|
You can apply individual sigils directly to data.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trix [sigil_name] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Sigils:**
|
||||||
|
|
||||||
|
* `reverse`
|
||||||
|
* `hex`
|
||||||
|
* `base64`
|
||||||
|
* `gzip`
|
||||||
|
* `json`, `json-indent`
|
||||||
|
* `md4`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`
|
||||||
|
* `ripemd160`
|
||||||
|
* `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`
|
||||||
|
* `sha512-224`, `sha512-256`
|
||||||
|
* `blake2s-256`, `blake2b-256`, `blake2b-384`, `blake2b-512`
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
|
||||||
|
* `-i, --input string`: Input file or string. Use `-` for stdin.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Base64 encode a string
|
||||||
|
trix base64 -i "hello world"
|
||||||
|
|
||||||
|
# Gzip a file
|
||||||
|
trix gzip -i myfile.txt > myfile.txt.gz
|
||||||
|
```
|
||||||
34
docs/hashing.md
Normal file
34
docs/hashing.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Hashing
|
||||||
|
|
||||||
|
This example demonstrates how to use the `crypt` service to hash a payload using various algorithms.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func demoHashing() {
|
||||||
|
fmt.Println("--- Hashing Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
payload := "Enchantrix"
|
||||||
|
|
||||||
|
hashTypes := []crypt.HashType{
|
||||||
|
crypt.LTHN,
|
||||||
|
crypt.MD5,
|
||||||
|
crypt.SHA1,
|
||||||
|
crypt.SHA256,
|
||||||
|
crypt.SHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Payload to hash: \"%s\"\n", payload)
|
||||||
|
for _, hashType := range hashTypes {
|
||||||
|
hash := cryptService.Hash(hashType, payload)
|
||||||
|
fmt.Printf(" - %-6s: %s\n", hashType, hash)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/index.md
Normal file
22
docs/index.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Welcome to Enchantrix
|
||||||
|
|
||||||
|
Enchantrix is a Go-based crypto library and miner application. This documentation provides information on how to use the various features of the Enchantrix library.
|
||||||
|
|
||||||
|
## Trix File Format
|
||||||
|
|
||||||
|
The `.trix` file format is a generic and flexible binary container for storing an arbitrary data payload alongside structured metadata. For more information, see the [Trix File Format](./trix_format.md) page.
|
||||||
|
|
||||||
|
## CLI Reference
|
||||||
|
|
||||||
|
Enchantrix provides a command-line tool for encoding, decoding, and hashing data. See the [CLI Reference](./cli.md) for detailed usage instructions.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The following pages provide examples of how to use the Enchantrix library:
|
||||||
|
|
||||||
|
* [Trix & Sigil Chaining](./trix_and_sigils.md)
|
||||||
|
* [Hashing](./hashing.md)
|
||||||
|
* [Checksums](./checksums.md)
|
||||||
|
* [RSA](./rsa.md)
|
||||||
|
* [PGP](./pgp.md)
|
||||||
|
* [Standalone Sigils](./standalone_sigils.md)
|
||||||
76
docs/pgp.md
Normal file
76
docs/pgp.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# PGP
|
||||||
|
|
||||||
|
This example demonstrates how to use the `crypt` service to perform PGP operations, including key generation, encryption, decryption, signing, and verification.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func demoPGP() {
|
||||||
|
fmt.Println("--- PGP Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Generate PGP Key Pair
|
||||||
|
fmt.Println("Generating PGP key pair...")
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("Alice", "alice@example.com", "Demo Key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("PGP Key pair generated successfully.")
|
||||||
|
|
||||||
|
// 2. Asymmetric Encryption (Public Key Encryption)
|
||||||
|
message := []byte("This is a secret message for PGP.")
|
||||||
|
fmt.Printf("\nOriginal message: %s\n", message)
|
||||||
|
|
||||||
|
encrypted, err := cryptService.EncryptPGP(publicKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Message encrypted.")
|
||||||
|
|
||||||
|
// 3. Decrypt with Private Key
|
||||||
|
decrypted, err := cryptService.DecryptPGP(privateKey, encrypted)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decrypted message: %s\n", decrypted)
|
||||||
|
|
||||||
|
if string(message) == string(decrypted) {
|
||||||
|
fmt.Println("Success! PGP decrypted message matches original.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Failure! PGP decrypted message does not match.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Signing and Verification
|
||||||
|
fmt.Println("\n--- PGP Signing Demo ---")
|
||||||
|
signature, err := cryptService.SignPGP(privateKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to sign message: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Message signed.")
|
||||||
|
|
||||||
|
err = cryptService.VerifyPGP(publicKey, message, signature)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to verify signature: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Success! Signature verified.")
|
||||||
|
|
||||||
|
// 5. Symmetric Encryption (Passphrase)
|
||||||
|
fmt.Println("\n--- PGP Symmetric Encryption Demo ---")
|
||||||
|
passphrase := []byte("super-secure-passphrase")
|
||||||
|
symEncrypted, err := cryptService.SymmetricallyEncryptPGP(passphrase, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to symmetrically encrypt: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Message symmetrically encrypted.")
|
||||||
|
// Note: Decryption of symmetrically encrypted PGP messages requires a compatible reader
|
||||||
|
// or usage of the underlying library's features, often handled automatically
|
||||||
|
// if the decryptor prompts for a passphrase.
|
||||||
|
}
|
||||||
|
```
|
||||||
52
docs/rsa.md
Normal file
52
docs/rsa.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# RSA
|
||||||
|
|
||||||
|
This example demonstrates how to use the `crypt` service to generate an RSA key pair, encrypt a message, and then decrypt it.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func demoRSA() {
|
||||||
|
fmt.Println("--- RSA Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Generate RSA key pair
|
||||||
|
fmt.Println("Generating 2048-bit RSA key pair...")
|
||||||
|
publicKey, privateKey, err := cryptService.GenerateRSAKeyPair(2048)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate RSA key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Key pair generated successfully.")
|
||||||
|
|
||||||
|
// 2. Encrypt a message
|
||||||
|
message := []byte("This is a secret message for RSA.")
|
||||||
|
fmt.Printf("\nOriginal message: %s\n", message)
|
||||||
|
ciphertext, err := cryptService.EncryptRSA(publicKey, message, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with RSA: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Encrypted ciphertext (base64): %s\n", base64.StdEncoding.EncodeToString(ciphertext))
|
||||||
|
|
||||||
|
// 3. Decrypt the message
|
||||||
|
decrypted, err := cryptService.DecryptRSA(privateKey, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decrypt with RSA: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decrypted message: %s\n", decrypted)
|
||||||
|
|
||||||
|
// 4. Verify
|
||||||
|
if string(message) == string(decrypted) {
|
||||||
|
fmt.Println("\nSuccess! RSA decrypted message matches the original.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nFailure! RSA decrypted message does not match the original.")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
```
|
||||||
74
docs/standalone_sigils.md
Normal file
74
docs/standalone_sigils.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Standalone Sigils
|
||||||
|
|
||||||
|
This example demonstrates how to use sigils independently to transform data.
|
||||||
|
|
||||||
|
## Available Sigils
|
||||||
|
|
||||||
|
The `enchantrix` package provides a wide variety of sigils for data transformation and hashing.
|
||||||
|
|
||||||
|
| Category | Sigils |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Encoding** | `hex`, `base64`, `reverse` |
|
||||||
|
| **Compression** | `gzip` |
|
||||||
|
| **Formatting** | `json`, `json-indent` |
|
||||||
|
| **Standard Hashes** | `md4`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512` |
|
||||||
|
| **Extended Hashes** | `ripemd160`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `sha512-224`, `sha512-256` |
|
||||||
|
| **Blake Hashes** | `blake2s-256`, `blake2b-256`, `blake2b-384`, `blake2b-512` |
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func demoSigils() {
|
||||||
|
fmt.Println("--- Standalone Sigil Demo ---")
|
||||||
|
data := []byte(`{"message": "hello world"}`)
|
||||||
|
fmt.Printf("Original data: %s\n", data)
|
||||||
|
|
||||||
|
// A chain of sigils to apply
|
||||||
|
sigils := []string{"gzip", "base64"}
|
||||||
|
fmt.Printf("Applying sigil chain: %v\n", sigils)
|
||||||
|
|
||||||
|
var transformedData = data
|
||||||
|
for _, name := range sigils {
|
||||||
|
s, err := enchantrix.NewSigil(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create sigil %s: %v", name, err)
|
||||||
|
}
|
||||||
|
transformedData, err = s.In(transformedData)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to apply sigil %s 'In': %v", name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" -> After '%s': %s\n", name, transformedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nReversing sigil chain...")
|
||||||
|
// Reverse the transformations
|
||||||
|
for i := len(sigils) - 1; i >= 0; i-- {
|
||||||
|
name := sigils[i]
|
||||||
|
s, err := enchantrix.NewSigil(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create sigil %s: %v", name, err)
|
||||||
|
}
|
||||||
|
transformedData, err = s.Out(transformedData)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to apply sigil %s 'Out': %v", name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" -> After '%s' Out: %s\n", name, transformedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(data) == string(transformedData) {
|
||||||
|
fmt.Println("Success! Data returned to original state.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Failure! Data did not return to original state.")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
```
|
||||||
124
docs/trix_and_sigils.md
Normal file
124
docs/trix_and_sigils.md
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Trix & Sigil Chaining
|
||||||
|
|
||||||
|
This example demonstrates how to use the Trix container with a chain of sigils to obfuscate and then encrypt a payload.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/trix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func demoTrix() {
|
||||||
|
fmt.Println("--- Trix & Sigil Chaining Demo ---")
|
||||||
|
|
||||||
|
// 1. Original plaintext (JSON data) and encryption key
|
||||||
|
type Message struct {
|
||||||
|
Author string `json:"author"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
originalMessage := Message{Author: "Jules", Time: time.Now().Unix(), Body: "This is a super secret message!"}
|
||||||
|
plaintext, err := json.Marshal(originalMessage)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
key := make([]byte, 32) // In a real application, use a secure key
|
||||||
|
for i := range key {
|
||||||
|
key[i] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Original Payload (JSON):\n%s\n\n", plaintext)
|
||||||
|
|
||||||
|
// 2. Create a Trix container with the plaintext and attach a chain of sigils
|
||||||
|
sigilChain := []string{"json-indent", "gzip", "base64", "reverse"}
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: plaintext,
|
||||||
|
InSigils: sigilChain,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pack the Trix container to apply the sigil transformations
|
||||||
|
fmt.Println("Packing payload with sigils:", sigilChain)
|
||||||
|
if err := trixContainer.Pack(); err != nil {
|
||||||
|
log.Fatalf("Failed to pack trix container: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Packed (obfuscated) payload is now non-human-readable bytes.\n\n")
|
||||||
|
|
||||||
|
// 4. Encrypt the packed payload
|
||||||
|
ciphertext, err := chachapoly.Encrypt(trixContainer.Payload, key)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt: %v", err)
|
||||||
|
}
|
||||||
|
trixContainer.Payload = ciphertext // Update the payload with the ciphertext
|
||||||
|
|
||||||
|
// 5. Add encryption metadata and checksum to the header
|
||||||
|
nonce := ciphertext[:24]
|
||||||
|
trixContainer.Header = map[string]interface{}{
|
||||||
|
"content_type": "application/json",
|
||||||
|
"encryption_algorithm": "chacha20poly1305",
|
||||||
|
"nonce": base64.StdEncoding.EncodeToString(nonce),
|
||||||
|
"created_at": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
trixContainer.ChecksumAlgo = crypt.SHA512
|
||||||
|
fmt.Printf("Checksum will be calculated with %s and added to the header.\n", trixContainer.ChecksumAlgo)
|
||||||
|
|
||||||
|
// 6. Encode the .trix container into its binary format
|
||||||
|
magicNumber := "MyT1"
|
||||||
|
encodedTrix, err := trix.Encode(trixContainer, magicNumber, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encode .trix container: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Successfully created .trix container.")
|
||||||
|
|
||||||
|
// --- DECODING ---
|
||||||
|
fmt.Println("--- DECODING ---")
|
||||||
|
|
||||||
|
// 7. Decode the .trix container
|
||||||
|
decodedTrix, err := trix.Decode(encodedTrix, magicNumber, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decode .trix container: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Successfully decoded .trix container. Checksum verified.")
|
||||||
|
fmt.Printf("Decoded Header: %+v\n", decodedTrix.Header)
|
||||||
|
|
||||||
|
// 8. Decrypt the payload
|
||||||
|
decryptedPayload, err := chachapoly.Decrypt(decodedTrix.Payload, key)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decrypt: %v", err)
|
||||||
|
}
|
||||||
|
decodedTrix.Payload = decryptedPayload
|
||||||
|
fmt.Println("Payload decrypted.")
|
||||||
|
|
||||||
|
// 9. Unpack the Trix container to reverse the sigil transformations
|
||||||
|
decodedTrix.InSigils = trixContainer.InSigils // Re-attach sigils for unpacking
|
||||||
|
fmt.Println("Unpacking payload by reversing sigils:", decodedTrix.InSigils)
|
||||||
|
if err := decodedTrix.Unpack(); err != nil {
|
||||||
|
log.Fatalf("Failed to unpack trix container: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Unpacked (original) payload:\n%s\n", decodedTrix.Payload)
|
||||||
|
|
||||||
|
// 10. Verify the result
|
||||||
|
// To properly verify, we need to compact the indented JSON before comparing
|
||||||
|
var compactedPayload bytes.Buffer
|
||||||
|
if err := json.Compact(&compactedPayload, decodedTrix.Payload); err != nil {
|
||||||
|
log.Fatalf("Failed to compact final payload for verification: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(plaintext, compactedPayload.Bytes()) {
|
||||||
|
fmt.Println("\nSuccess! The message was decrypted and unpacked correctly.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nFailure! The final payload does not match the original.")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
```
|
||||||
33
examples/checksums/main.go
Normal file
33
examples/checksums/main.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- Checksum Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// Luhn
|
||||||
|
luhnPayloadGood := "49927398716"
|
||||||
|
luhnPayloadBad := "49927398717"
|
||||||
|
fmt.Printf("Luhn Checksum:\n")
|
||||||
|
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadGood, cryptService.Luhn(luhnPayloadGood))
|
||||||
|
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadBad, cryptService.Luhn(luhnPayloadBad))
|
||||||
|
|
||||||
|
// Fletcher
|
||||||
|
fletcherPayload := "abcde"
|
||||||
|
fmt.Printf("\nFletcher Checksums (Payload: \"%s\"):\n", fletcherPayload)
|
||||||
|
fmt.Printf(" - Fletcher16: %d\n", cryptService.Fletcher16(fletcherPayload))
|
||||||
|
fmt.Printf(" - Fletcher32: %d\n", cryptService.Fletcher32(fletcherPayload))
|
||||||
|
fmt.Printf(" - Fletcher64: %d\n", cryptService.Fletcher64(fletcherPayload))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
21
examples/coverage_report/main.go
Normal file
21
examples/coverage_report/main.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- Test Coverage Demo ---")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("This example demonstrates how to generate and interpret a test coverage report.")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("1. Generate a coverage profile:")
|
||||||
|
fmt.Println(" go test ./... -coverprofile=coverage.out")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("2. View the coverage report in your browser:")
|
||||||
|
fmt.Println(" go tool cover -html=coverage.out")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("3. View the coverage report in your terminal:")
|
||||||
|
fmt.Println(" go tool cover -func=coverage.out")
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
57
examples/examples_test.go
Normal file
57
examples/examples_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package examples_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExample_Checksums(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./checksums"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample_Hash(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./hash"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample_PGPEncryptDecrypt(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./pgp_encrypt_decrypt"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample_PGPGenerateKeys(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./pgp_generate_keys"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample_PGPSignVerify(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./pgp_sign_verify"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample_PGPSymmetricEncrypt(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./pgp_symmetric_encrypt"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample_RSA(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", "run", ".")
|
||||||
|
cmd.Dir = "./rsa"
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
assert.NoError(t, err, string(out))
|
||||||
|
}
|
||||||
34
examples/hash/main.go
Normal file
34
examples/hash/main.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- Hashing Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
payload := "Enchantrix"
|
||||||
|
|
||||||
|
hashTypes := []crypt.HashType{
|
||||||
|
crypt.LTHN,
|
||||||
|
crypt.MD5,
|
||||||
|
crypt.SHA1,
|
||||||
|
crypt.SHA256,
|
||||||
|
crypt.SHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Payload to hash: \"%s\"\n", payload)
|
||||||
|
for _, hashType := range hashTypes {
|
||||||
|
hash := cryptService.Hash(hashType, payload)
|
||||||
|
fmt.Printf(" - %-6s: %s\n", hashType, hash)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly"
|
|
||||||
"github.com/Snider/Enchantrix/pkg/trix"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 1. Original plaintext and encryption key
|
|
||||||
plaintext := []byte("This is a super secret message!")
|
|
||||||
key := make([]byte, 32) // In a real application, use a secure key
|
|
||||||
for i := range key {
|
|
||||||
key[i] = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create a Trix container with the plaintext and attach sigils
|
|
||||||
trixContainer := &trix.Trix{
|
|
||||||
Header: map[string]interface{}{},
|
|
||||||
Payload: plaintext,
|
|
||||||
Sigils: []trix.Sigil{&trix.ReverseSigil{}},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Pack the Trix container to apply the sigil transformations
|
|
||||||
if err := trixContainer.Pack(); err != nil {
|
|
||||||
log.Fatalf("Failed to pack trix container: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Packed (obfuscated) payload: %x\n", trixContainer.Payload)
|
|
||||||
|
|
||||||
|
|
||||||
// 4. Encrypt the packed payload
|
|
||||||
ciphertext, err := chachapoly.Encrypt(trixContainer.Payload, key)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to encrypt: %v", err)
|
|
||||||
}
|
|
||||||
trixContainer.Payload = ciphertext // Update the payload with the ciphertext
|
|
||||||
|
|
||||||
// 5. Add encryption metadata to the header
|
|
||||||
nonce := ciphertext[:24]
|
|
||||||
trixContainer.Header = map[string]interface{}{
|
|
||||||
"content_type": "application/octet-stream",
|
|
||||||
"encryption_algorithm": "chacha20poly1305",
|
|
||||||
"nonce": base64.StdEncoding.EncodeToString(nonce),
|
|
||||||
"created_at": time.Now().UTC().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 6. Encode the .trix container into its binary format
|
|
||||||
magicNumber := "MyT1"
|
|
||||||
encodedTrix, err := trix.Encode(trixContainer, magicNumber)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to encode .trix container: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("Successfully created .trix container.")
|
|
||||||
|
|
||||||
// --- DECODING ---
|
|
||||||
|
|
||||||
// 7. Decode the .trix container
|
|
||||||
decodedTrix, err := trix.Decode(encodedTrix, magicNumber)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to decode .trix container: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Decrypt the payload
|
|
||||||
decryptedPayload, err := chachapoly.Decrypt(decodedTrix.Payload, key)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to decrypt: %v", err)
|
|
||||||
}
|
|
||||||
decodedTrix.Payload = decryptedPayload
|
|
||||||
|
|
||||||
// 9. Unpack the Trix container to reverse the sigil transformations
|
|
||||||
decodedTrix.Sigils = trixContainer.Sigils // Re-attach sigils
|
|
||||||
if err := decodedTrix.Unpack(); err != nil {
|
|
||||||
log.Fatalf("Failed to unpack trix container: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Unpacked (original) payload: %s\n", decodedTrix.Payload)
|
|
||||||
|
|
||||||
// 10. Verify the result
|
|
||||||
if string(plaintext) == string(decodedTrix.Payload) {
|
|
||||||
fmt.Println("\nSuccess! The message was decrypted and unpacked correctly.")
|
|
||||||
} else {
|
|
||||||
fmt.Println("\nFailure! The final payload does not match the original.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
examples/pgp_encrypt_decrypt/main.go
Normal file
52
examples/pgp_encrypt_decrypt/main.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- PGP Encryption & Decryption Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Generate PGP key pair
|
||||||
|
fmt.Println("Generating PGP key pair...")
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Key pair generated successfully.")
|
||||||
|
|
||||||
|
// 2. Encrypt a message
|
||||||
|
message := []byte("This is a secret message for PGP.")
|
||||||
|
fmt.Printf("\nOriginal message: %s\n", message)
|
||||||
|
ciphertext, err := cryptService.EncryptPGP(publicKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Encrypted ciphertext (armored):\n%s\n", ciphertext)
|
||||||
|
|
||||||
|
// 3. Decrypt the message
|
||||||
|
decrypted, err := cryptService.DecryptPGP(privateKey, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decrypted message: %s\n", decrypted)
|
||||||
|
|
||||||
|
// 4. Verify
|
||||||
|
if string(message) == string(decrypted) {
|
||||||
|
fmt.Println("\nSuccess! PGP decrypted message matches the original.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nFailure! PGP decrypted message does not match the original.")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
30
examples/pgp_generate_keys/main.go
Normal file
30
examples/pgp_generate_keys/main.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- PGP Key Generation Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Generate PGP key pair
|
||||||
|
fmt.Println("Generating PGP key pair...")
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Key pair generated successfully.")
|
||||||
|
fmt.Printf("\nPublic Key:\n%s\n", publicKey)
|
||||||
|
fmt.Printf("\nPrivate Key:\n%s\n", privateKey)
|
||||||
|
}
|
||||||
45
examples/pgp_sign_verify/main.go
Normal file
45
examples/pgp_sign_verify/main.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- PGP Signing & Verification Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Generate PGP key pair
|
||||||
|
fmt.Println("Generating PGP key pair...")
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Key pair generated successfully.")
|
||||||
|
|
||||||
|
// 2. Sign a message
|
||||||
|
message := []byte("This is a message to be signed.")
|
||||||
|
fmt.Printf("\nOriginal message: %s\n", message)
|
||||||
|
signature, err := cryptService.SignPGP(privateKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to sign with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Signature (armored):\n%s\n", signature)
|
||||||
|
|
||||||
|
// 3. Verify the signature
|
||||||
|
err = cryptService.VerifyPGP(publicKey, message, signature)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to verify signature: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Signature verified successfully!")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
31
examples/pgp_symmetric_encrypt/main.go
Normal file
31
examples/pgp_symmetric_encrypt/main.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- PGP Symmetric Encryption Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Encrypt a message with a passphrase
|
||||||
|
message := []byte("This is a secret message for symmetric PGP encryption.")
|
||||||
|
passphrase := []byte("my-secret-passphrase")
|
||||||
|
fmt.Printf("\nOriginal message: %s\n", message)
|
||||||
|
ciphertext, err := cryptService.SymmetricallyEncryptPGP(passphrase, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Encrypted ciphertext (armored):\n%s\n", ciphertext)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
53
examples/rsa/main.go
Normal file
53
examples/rsa/main.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// 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 (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("--- RSA Demo ---")
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
|
||||||
|
// 1. Generate RSA key pair
|
||||||
|
fmt.Println("Generating 2048-bit RSA key pair...")
|
||||||
|
publicKey, privateKey, err := cryptService.GenerateRSAKeyPair(2048)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate RSA key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Key pair generated successfully.")
|
||||||
|
|
||||||
|
// 2. Encrypt a message
|
||||||
|
message := []byte("This is a secret message for RSA.")
|
||||||
|
fmt.Printf("\nOriginal message: %s\n", message)
|
||||||
|
ciphertext, err := cryptService.EncryptRSA(publicKey, message, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with RSA: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Encrypted ciphertext (base64): %s\n", base64.StdEncoding.EncodeToString(ciphertext))
|
||||||
|
|
||||||
|
// 3. Decrypt the message
|
||||||
|
decrypted, err := cryptService.DecryptRSA(privateKey, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decrypt with RSA: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decrypted message: %s\n", decrypted)
|
||||||
|
|
||||||
|
// 4. Verify
|
||||||
|
if string(message) == string(decrypted) {
|
||||||
|
fmt.Println("\nSuccess! RSA decrypted message matches the original.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nFailure! RSA decrypted message does not match the original.")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
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!")
|
||||||
|
}
|
||||||
37
go.mod
37
go.mod
|
|
@ -3,45 +3,18 @@ module github.com/Snider/Enchantrix
|
||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/ProtonMail/go-crypto v1.3.0
|
||||||
github.com/leaanthony/clir v1.7.0
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.43.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
|
||||||
golang.org/x/net v0.45.0 // indirect
|
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
93
go.sum
93
go.sum
|
|
@ -1,95 +1,26 @@
|
||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
|
||||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/leaanthony/clir v1.7.0 h1:xiAnhl7ryPwuH3ERwPWZp/pCHk8wTeiwuAOt6MiNyAw=
|
|
||||||
github.com/leaanthony/clir v1.7.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
|
||||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
|
||||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
|
||||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
12
go.work.sum
Normal file
12
go.work.sum
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
|
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
|
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||||
|
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
24
mkdocs.yml
Normal file
24
mkdocs.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
site_name: Enchantrix
|
||||||
|
site_description: Go encryption library and Trix file format
|
||||||
|
site_url: https://github.com/Snider/Enchantrix
|
||||||
|
repo_url: https://github.com/Snider/Enchantrix
|
||||||
|
repo_name: Snider/Enchantrix
|
||||||
|
docs_dir: docs
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Trix File Format: trix_format.md
|
||||||
|
- CLI Reference: cli.md
|
||||||
|
- Examples:
|
||||||
|
- Trix & Sigil Chaining: trix_and_sigils.md
|
||||||
|
- Hashing: hashing.md
|
||||||
|
- Checksums: checksums.md
|
||||||
|
- RSA: rsa.md
|
||||||
|
- PGP: pgp.md
|
||||||
|
- Standalone Sigils: standalone_sigils.md
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- codehilite
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
API struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
WorkerID string `json:"worker-id"`
|
|
||||||
} `json:"api"`
|
|
||||||
HTTP struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
AccessToken string `json:"access-token"`
|
|
||||||
Restricted bool `json:"restricted"`
|
|
||||||
} `json:"http"`
|
|
||||||
Pools []struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
User string `json:"user"`
|
|
||||||
Pass string `json:"pass"`
|
|
||||||
} `json:"pools"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Config {
|
|
||||||
cfg := &Config{}
|
|
||||||
if _, err := os.Stat("config.json"); err == nil {
|
|
||||||
cfg.Load("config.json")
|
|
||||||
} else {
|
|
||||||
cfg.API.ID = "enchantrix"
|
|
||||||
cfg.HTTP.Enabled = true
|
|
||||||
cfg.HTTP.Host = "127.0.0.1"
|
|
||||||
cfg.HTTP.Port = 8080
|
|
||||||
cfg.HTTP.Restricted = true
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Load(path string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Unmarshal(data, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Get() *Config {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
// To avoid returning a pointer to the internal struct, we create a copy
|
|
||||||
clone := *c
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Update(newConfig *Config) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.API = newConfig.API
|
|
||||||
c.HTTP = newConfig.HTTP
|
|
||||||
c.Pools = newConfig.Pools
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfig(t *testing.T) {
|
|
||||||
cfg := New()
|
|
||||||
assert.NotNil(t, cfg)
|
|
||||||
assert.Equal(t, 8080, cfg.HTTP.Port)
|
|
||||||
|
|
||||||
newConfig := New()
|
|
||||||
newConfig.HTTP.Port = 8081
|
|
||||||
cfg.Update(newConfig)
|
|
||||||
|
|
||||||
assert.Equal(t, 8081, cfg.Get().HTTP.Port)
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@ package crypt
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"errors"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
@ -11,30 +12,56 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Snider/Enchantrix/pkg/crypt/std/lthn"
|
"github.com/Snider/Enchantrix/pkg/crypt/std/lthn"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt/std/pgp"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt/std/rsa"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is the main struct for the crypt service.
|
// Service is the main struct for the crypt service.
|
||||||
type Service struct{}
|
// It provides methods for hashing, checksums, and encryption.
|
||||||
|
type Service struct {
|
||||||
|
rsa *rsa.Service
|
||||||
|
pgp *pgp.Service
|
||||||
|
}
|
||||||
|
|
||||||
// NewService creates a new crypt service.
|
// NewService creates a new crypt Service and initialises its embedded services.
|
||||||
|
// It returns a new Service.
|
||||||
func NewService() *Service {
|
func NewService() *Service {
|
||||||
return &Service{}
|
return &Service{
|
||||||
|
rsa: rsa.NewService(),
|
||||||
|
pgp: pgp.NewService(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashType defines the supported hashing algorithms.
|
// HashType defines the supported hashing algorithms.
|
||||||
type HashType string
|
type HashType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LTHN HashType = "lthn"
|
// LTHN is a custom quasi-salted hashing algorithm.
|
||||||
|
LTHN HashType = "lthn"
|
||||||
|
// SHA512 is the SHA-512 hashing algorithm.
|
||||||
SHA512 HashType = "sha512"
|
SHA512 HashType = "sha512"
|
||||||
|
// SHA256 is the SHA-256 hashing algorithm.
|
||||||
SHA256 HashType = "sha256"
|
SHA256 HashType = "sha256"
|
||||||
SHA1 HashType = "sha1"
|
// SHA1 is the SHA-1 hashing algorithm.
|
||||||
MD5 HashType = "md5"
|
SHA1 HashType = "sha1"
|
||||||
|
// MD5 is the MD5 hashing algorithm.
|
||||||
|
MD5 HashType = "md5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Hashing ---
|
// --- Hashing ---
|
||||||
|
|
||||||
|
// IsHashAlgo checks if a string is a valid hash algorithm.
|
||||||
|
func (s *Service) IsHashAlgo(algo string) bool {
|
||||||
|
switch HashType(algo) {
|
||||||
|
case LTHN, SHA512, SHA256, SHA1, MD5:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Hash computes a hash of the payload using the specified algorithm.
|
// Hash computes a hash of the payload using the specified algorithm.
|
||||||
|
// It returns the hash as a hex-encoded string.
|
||||||
func (s *Service) Hash(lib HashType, payload string) string {
|
func (s *Service) Hash(lib HashType, payload string) string {
|
||||||
switch lib {
|
switch lib {
|
||||||
case LTHN:
|
case LTHN:
|
||||||
|
|
@ -59,6 +86,7 @@ func (s *Service) Hash(lib HashType, payload string) string {
|
||||||
// --- Checksums ---
|
// --- Checksums ---
|
||||||
|
|
||||||
// Luhn validates a number using the Luhn algorithm.
|
// Luhn validates a number using the Luhn algorithm.
|
||||||
|
// It is typically used to validate credit card numbers.
|
||||||
func (s *Service) Luhn(payload string) bool {
|
func (s *Service) Luhn(payload string) bool {
|
||||||
payload = strings.ReplaceAll(payload, " ", "")
|
payload = strings.ReplaceAll(payload, " ", "")
|
||||||
if len(payload) <= 1 {
|
if len(payload) <= 1 {
|
||||||
|
|
@ -87,6 +115,7 @@ func (s *Service) Luhn(payload string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fletcher16 computes the Fletcher-16 checksum.
|
// Fletcher16 computes the Fletcher-16 checksum.
|
||||||
|
// It is a fast checksum algorithm that is more reliable than a simple sum.
|
||||||
func (s *Service) Fletcher16(payload string) uint16 {
|
func (s *Service) Fletcher16(payload string) uint16 {
|
||||||
data := []byte(payload)
|
data := []byte(payload)
|
||||||
var sum1, sum2 uint16
|
var sum1, sum2 uint16
|
||||||
|
|
@ -98,6 +127,7 @@ func (s *Service) Fletcher16(payload string) uint16 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fletcher32 computes the Fletcher-32 checksum.
|
// Fletcher32 computes the Fletcher-32 checksum.
|
||||||
|
// It provides better error detection than Fletcher-16.
|
||||||
func (s *Service) Fletcher32(payload string) uint32 {
|
func (s *Service) Fletcher32(payload string) uint32 {
|
||||||
data := []byte(payload)
|
data := []byte(payload)
|
||||||
if len(data)%2 != 0 {
|
if len(data)%2 != 0 {
|
||||||
|
|
@ -114,6 +144,7 @@ func (s *Service) Fletcher32(payload string) uint32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fletcher64 computes the Fletcher-64 checksum.
|
// Fletcher64 computes the Fletcher-64 checksum.
|
||||||
|
// It provides the best error detection of the Fletcher algorithms.
|
||||||
func (s *Service) Fletcher64(payload string) uint64 {
|
func (s *Service) Fletcher64(payload string) uint64 {
|
||||||
data := []byte(payload)
|
data := []byte(payload)
|
||||||
if len(data)%4 != 0 {
|
if len(data)%4 != 0 {
|
||||||
|
|
@ -130,31 +161,83 @@ func (s *Service) Fletcher64(payload string) uint64 {
|
||||||
return (sum2 << 32) | sum1
|
return (sum2 << 32) | sum1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- RSA ---
|
||||||
|
|
||||||
|
// ensureRSA initializes the RSA service if it is not already.
|
||||||
|
func (s *Service) ensureRSA() {
|
||||||
|
if s.rsa == nil {
|
||||||
|
s.rsa = rsa.NewService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRSAKeyPair creates a new RSA key pair.
|
||||||
|
func (s *Service) GenerateRSAKeyPair(bits int) (publicKey, privateKey []byte, err error) {
|
||||||
|
s.ensureRSA()
|
||||||
|
return s.rsa.GenerateKeyPair(bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptRSA encrypts data with a public key.
|
||||||
|
func (s *Service) EncryptRSA(publicKey, data, label []byte) ([]byte, error) {
|
||||||
|
s.ensureRSA()
|
||||||
|
return s.rsa.Encrypt(publicKey, data, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptRSA decrypts data with a private key.
|
||||||
|
func (s *Service) DecryptRSA(privateKey, ciphertext, label []byte) ([]byte, error) {
|
||||||
|
s.ensureRSA()
|
||||||
|
return s.rsa.Decrypt(privateKey, ciphertext, label)
|
||||||
|
}
|
||||||
|
|
||||||
// --- PGP ---
|
// --- PGP ---
|
||||||
|
|
||||||
// @snider
|
// ensurePGP initializes the PGP service if it is not already.
|
||||||
// The PGP functions are commented out pending resolution of the dependency issues.
|
func (s *Service) ensurePGP() {
|
||||||
//
|
if s.pgp == nil {
|
||||||
// import "io"
|
s.pgp = pgp.NewService()
|
||||||
// import "github.com/Snider/Enchantrix/openpgp"
|
}
|
||||||
//
|
}
|
||||||
// // EncryptPGP encrypts data for a recipient, optionally signing it.
|
|
||||||
// func (s *Service) EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error {
|
// GeneratePGPKeyPair creates a new PGP key pair.
|
||||||
// var buf bytes.Buffer
|
// It returns the public and private keys in PEM format.
|
||||||
// err := openpgp.EncryptPGP(&buf, recipientPath, data, signerPath, signerPassphrase)
|
func (s *Service) GeneratePGPKeyPair(name, email, comment string) (publicKey, privateKey []byte, err error) {
|
||||||
// if err != nil {
|
s.ensurePGP()
|
||||||
// return err
|
return s.pgp.GenerateKeyPair(name, email, comment)
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// // Copy the encrypted data to the original writer.
|
// EncryptPGP encrypts data with a public key.
|
||||||
// if _, err := writer.Write(buf.Bytes()); err != nil {
|
// It returns the encrypted data.
|
||||||
// return err
|
func (s *Service) EncryptPGP(publicKey, data []byte) ([]byte, error) {
|
||||||
// }
|
s.ensurePGP()
|
||||||
//
|
return s.pgp.Encrypt(publicKey, data)
|
||||||
// return nil
|
}
|
||||||
// }
|
|
||||||
//
|
// DecryptPGP decrypts data with a private key.
|
||||||
// // DecryptPGP decrypts a PGP message, optionally verifying the signature.
|
// It returns the decrypted data.
|
||||||
// func (s *Service) DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
|
func (s *Service) DecryptPGP(privateKey, ciphertext []byte) ([]byte, error) {
|
||||||
// return openpgp.DecryptPGP(recipientPath, message, passphrase, signerPath)
|
s.ensurePGP()
|
||||||
// }
|
return s.pgp.Decrypt(privateKey, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignPGP creates a detached signature for a message.
|
||||||
|
// It returns the signature.
|
||||||
|
func (s *Service) SignPGP(privateKey, data []byte) ([]byte, error) {
|
||||||
|
s.ensurePGP()
|
||||||
|
return s.pgp.Sign(privateKey, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPGP verifies a detached signature for a message.
|
||||||
|
// It returns an error if the signature is invalid.
|
||||||
|
func (s *Service) VerifyPGP(publicKey, data, signature []byte) error {
|
||||||
|
s.ensurePGP()
|
||||||
|
return s.pgp.Verify(publicKey, data, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SymmetricallyEncryptPGP encrypts data with a passphrase.
|
||||||
|
// It returns the encrypted data.
|
||||||
|
func (s *Service) SymmetricallyEncryptPGP(passphrase, data []byte) ([]byte, error) {
|
||||||
|
s.ensurePGP()
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
return nil, errors.New("passphrase cannot be empty")
|
||||||
|
}
|
||||||
|
return s.pgp.SymmetricallyEncrypt(passphrase, data)
|
||||||
|
}
|
||||||
|
|
|
||||||
33
pkg/crypt/crypt_internal_test.go
Normal file
33
pkg/crypt/crypt_internal_test.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package crypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEnsureRSA_Good tests that the RSA service is initialized correctly.
|
||||||
|
func TestEnsureRSA_Good(t *testing.T) {
|
||||||
|
s := &Service{}
|
||||||
|
s.ensureRSA()
|
||||||
|
assert.NotNil(t, s.rsa)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnsureRSA_Bad tests that calling ensureRSA multiple times does not change the RSA service.
|
||||||
|
func TestEnsureRSA_Bad(t *testing.T) {
|
||||||
|
s := &Service{}
|
||||||
|
s.ensureRSA()
|
||||||
|
rsa1 := s.rsa
|
||||||
|
s.ensureRSA()
|
||||||
|
rsa2 := s.rsa
|
||||||
|
assert.Same(t, rsa1, rsa2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnsureRSA_Ugly tests that ensureRSA works correctly on a service with a pre-initialized RSA service.
|
||||||
|
func TestEnsureRSA_Ugly(t *testing.T) {
|
||||||
|
s := NewService() // NewService initializes the RSA service
|
||||||
|
rsa1 := s.rsa
|
||||||
|
s.ensureRSA()
|
||||||
|
rsa2 := s.rsa
|
||||||
|
assert.Same(t, rsa1, rsa2)
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
package crypt
|
package crypt_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var service = NewService()
|
var service = crypt.NewService()
|
||||||
|
|
||||||
// --- Hashing Tests ---
|
// --- Hashing Tests ---
|
||||||
|
|
||||||
func TestHash_Good(t *testing.T) {
|
func TestHash_Good(t *testing.T) {
|
||||||
payload := "hello"
|
payload := "hello"
|
||||||
// Test all supported hash types
|
// Test all supported hash types
|
||||||
for _, hashType := range []HashType{LTHN, SHA512, SHA256, SHA1, MD5} {
|
for _, hashType := range []crypt.HashType{crypt.LTHN, crypt.SHA512, crypt.SHA256, crypt.SHA1, crypt.MD5} {
|
||||||
hash := service.Hash(hashType, payload)
|
hash := service.Hash(hashType, payload)
|
||||||
assert.NotEmpty(t, hash, "Hash should not be empty for type %s", hashType)
|
assert.NotEmpty(t, hash, "Hash should not be empty for type %s", hashType)
|
||||||
}
|
}
|
||||||
|
|
@ -23,22 +24,22 @@ func TestHash_Good(t *testing.T) {
|
||||||
func TestHash_Bad(t *testing.T) {
|
func TestHash_Bad(t *testing.T) {
|
||||||
// Using an unsupported hash type should default to SHA256
|
// Using an unsupported hash type should default to SHA256
|
||||||
hash := service.Hash("unsupported", "hello")
|
hash := service.Hash("unsupported", "hello")
|
||||||
expectedHash := service.Hash(SHA256, "hello")
|
expectedHash := service.Hash(crypt.SHA256, "hello")
|
||||||
assert.Equal(t, expectedHash, hash)
|
assert.Equal(t, expectedHash, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHash_Ugly(t *testing.T) {
|
func TestHash_Ugly(t *testing.T) {
|
||||||
// Test with potentially problematic inputs
|
// Test with potentially problematic inputs
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
"", // Empty string
|
"", // Empty string
|
||||||
" ", // Whitespace
|
" ", // Whitespace
|
||||||
"\x00\x01\x02\x03\x04", // Null bytes
|
"\x00\x01\x02\x03\x04", // Null bytes
|
||||||
strings.Repeat("a", 1024*1024), // Large payload (1MB)
|
strings.Repeat("a", 1024*1024), // Large payload (1MB)
|
||||||
"こんにちは", // Unicode characters
|
"こんにちは", // Unicode characters
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
for _, hashType := range []HashType{LTHN, SHA512, SHA256, SHA1, MD5} {
|
for _, hashType := range []crypt.HashType{crypt.LTHN, crypt.SHA512, crypt.SHA256, crypt.SHA1, crypt.MD5} {
|
||||||
hash := service.Hash(hashType, tc)
|
hash := service.Hash(hashType, tc)
|
||||||
assert.NotEmpty(t, hash, "Hash for ugly input should not be empty for type %s", hashType)
|
assert.NotEmpty(t, hash, "Hash for ugly input should not be empty for type %s", hashType)
|
||||||
}
|
}
|
||||||
|
|
@ -55,11 +56,13 @@ func TestLuhn_Good(t *testing.T) {
|
||||||
func TestLuhn_Bad(t *testing.T) {
|
func TestLuhn_Bad(t *testing.T) {
|
||||||
assert.False(t, service.Luhn("79927398714"), "Should fail for incorrect checksum")
|
assert.False(t, service.Luhn("79927398714"), "Should fail for incorrect checksum")
|
||||||
assert.False(t, service.Luhn("7992739871a"), "Should fail for non-numeric input")
|
assert.False(t, service.Luhn("7992739871a"), "Should fail for non-numeric input")
|
||||||
|
assert.False(t, service.Luhn("1"), "Should be false for single digit")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLuhn_Ugly(t *testing.T) {
|
func TestLuhn_Ugly(t *testing.T) {
|
||||||
assert.False(t, service.Luhn(""), "Should be false for empty string")
|
assert.False(t, service.Luhn(""), "Should be false for empty string")
|
||||||
assert.False(t, service.Luhn(" 1 2 3 "), "Should handle spaces but result in false")
|
assert.False(t, service.Luhn(" 1 2 3 "), "Should handle spaces but result in false")
|
||||||
|
assert.False(t, service.Luhn("!@#$%^&*()"), "Should be false for special characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fletcher16 Tests
|
// Fletcher16 Tests
|
||||||
|
|
@ -69,13 +72,10 @@ func TestFletcher16_Good(t *testing.T) {
|
||||||
assert.Equal(t, uint16(0x0627), service.Fletcher16("abcdefgh"))
|
assert.Equal(t, uint16(0x0627), service.Fletcher16("abcdefgh"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFletcher16_Bad(t *testing.T) {
|
|
||||||
// No obviously "bad" inputs that don't fall into "ugly"
|
|
||||||
// For Fletcher, any string is a valid input.
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFletcher16_Ugly(t *testing.T) {
|
func TestFletcher16_Ugly(t *testing.T) {
|
||||||
assert.Equal(t, uint16(0), service.Fletcher16(""), "Checksum of empty string should be 0")
|
assert.Equal(t, uint16(0), service.Fletcher16(""), "Checksum of empty string should be 0")
|
||||||
|
assert.Equal(t, uint16(0), service.Fletcher16("\x00"), "Checksum of null byte should be 0")
|
||||||
|
assert.NotEqual(t, uint16(0), service.Fletcher16(" "), "Checksum of space should not be 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fletcher32 Tests
|
// Fletcher32 Tests
|
||||||
|
|
@ -85,12 +85,11 @@ func TestFletcher32_Good(t *testing.T) {
|
||||||
assert.Equal(t, uint32(0xEBE19591), service.Fletcher32("abcdefgh"))
|
assert.Equal(t, uint32(0xEBE19591), service.Fletcher32("abcdefgh"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFletcher32_Bad(t *testing.T) {
|
|
||||||
// Any string is a valid input.
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFletcher32_Ugly(t *testing.T) {
|
func TestFletcher32_Ugly(t *testing.T) {
|
||||||
assert.Equal(t, uint32(0), service.Fletcher32(""), "Checksum of empty string should be 0")
|
assert.Equal(t, uint32(0), service.Fletcher32(""), "Checksum of empty string should be 0")
|
||||||
|
// Test odd length string to check padding
|
||||||
|
assert.NotEqual(t, uint32(0), service.Fletcher32("a"), "Checksum of odd length string")
|
||||||
|
assert.NotEqual(t, uint32(0), service.Fletcher32(" "), "Checksum of space should not be 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fletcher64 Tests
|
// Fletcher64 Tests
|
||||||
|
|
@ -100,10 +99,173 @@ func TestFletcher64_Good(t *testing.T) {
|
||||||
assert.Equal(t, uint64(0x312e2b28cccac8c6), service.Fletcher64("abcdefgh"))
|
assert.Equal(t, uint64(0x312e2b28cccac8c6), service.Fletcher64("abcdefgh"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFletcher64_Bad(t *testing.T) {
|
|
||||||
// Any string is a valid input.
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFletcher64_Ugly(t *testing.T) {
|
func TestFletcher64_Ugly(t *testing.T) {
|
||||||
assert.Equal(t, uint64(0), service.Fletcher64(""), "Checksum of empty string should be 0")
|
assert.Equal(t, uint64(0), service.Fletcher64(""), "Checksum of empty string should be 0")
|
||||||
|
// Test different length strings to check padding
|
||||||
|
assert.NotEqual(t, uint64(0), service.Fletcher64("a"), "Checksum of length 1 string")
|
||||||
|
assert.NotEqual(t, uint64(0), service.Fletcher64("ab"), "Checksum of length 2 string")
|
||||||
|
assert.NotEqual(t, uint64(0), service.Fletcher64("abc"), "Checksum of length 3 string")
|
||||||
|
assert.NotEqual(t, uint64(0), service.Fletcher64(" "), "Checksum of space should not be 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RSA Tests ---
|
||||||
|
|
||||||
|
func TestRSA_Good(t *testing.T) {
|
||||||
|
pubKey, privKey, err := service.GenerateRSAKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, pubKey)
|
||||||
|
assert.NotNil(t, privKey)
|
||||||
|
|
||||||
|
// Test encryption and decryption
|
||||||
|
message := []byte("secret message")
|
||||||
|
label := []byte("test label")
|
||||||
|
ciphertext, err := service.EncryptRSA(pubKey, message, label)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
plaintext, err := service.DecryptRSA(privKey, ciphertext, label)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, message, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PGP Tests ---
|
||||||
|
|
||||||
|
func TestPGP_Good(t *testing.T) {
|
||||||
|
pubKey, privKey, err := service.GeneratePGPKeyPair("test", "test@test.com", "test comment")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, pubKey)
|
||||||
|
assert.NotNil(t, privKey)
|
||||||
|
|
||||||
|
// Test encryption and decryption
|
||||||
|
message := []byte("secret message")
|
||||||
|
ciphertext, err := service.EncryptPGP(pubKey, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
plaintext, err := service.DecryptPGP(privKey, ciphertext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, message, plaintext)
|
||||||
|
|
||||||
|
// Test signing and verification
|
||||||
|
signature, err := service.SignPGP(privKey, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = service.VerifyPGP(pubKey, message, signature)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test symmetric encryption
|
||||||
|
passphrase := []byte("my-secret-passphrase")
|
||||||
|
ciphertext, err = service.SymmetricallyEncryptPGP(passphrase, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPGP_Bad(t *testing.T) {
|
||||||
|
// Generate two key pairs
|
||||||
|
pubKey1, privKey1, err := service.GeneratePGPKeyPair("test1", "test1@test.com", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
pubKey2, privKey2, err := service.GeneratePGPKeyPair("test2", "test2@test.com", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
message := []byte("secret message")
|
||||||
|
|
||||||
|
// Test decryption with the wrong key
|
||||||
|
ciphertext, err := service.EncryptPGP(pubKey1, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// This should fail because we are using the wrong private key.
|
||||||
|
_, err = service.DecryptPGP(privKey2, ciphertext) // Intentionally using wrong key
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test verification with the wrong key
|
||||||
|
signature, err := service.SignPGP(privKey1, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = service.VerifyPGP(pubKey2, message, signature)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test verification with a tampered message
|
||||||
|
tamperedMessage := []byte("tampered message")
|
||||||
|
err = service.VerifyPGP(pubKey1, tamperedMessage, signature)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPGP_Ugly(t *testing.T) {
|
||||||
|
// Test with malformed keys
|
||||||
|
_, err := service.EncryptPGP([]byte("not a real key"), []byte("message"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = service.DecryptPGP([]byte("not a real key"), []byte("message"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = service.SignPGP([]byte("not a real key"), []byte("message"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = service.VerifyPGP([]byte("not a real key"), []byte("message"), []byte("not a real signature"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with empty message
|
||||||
|
pubKey, privKey, err := service.GeneratePGPKeyPair("test", "test@test.com", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
message := []byte("")
|
||||||
|
ciphertext, err := service.EncryptPGP(pubKey, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
plaintext, err := service.DecryptPGP(privKey, ciphertext, )
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, message, plaintext)
|
||||||
|
|
||||||
|
// Test symmetric encryption with empty passphrase
|
||||||
|
_, err = service.SymmetricallyEncryptPGP([]byte(""), message)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- IsHashAlgo Tests ---
|
||||||
|
|
||||||
|
func TestIsHashAlgo_Good(t *testing.T) {
|
||||||
|
assert.True(t, service.IsHashAlgo("lthn"))
|
||||||
|
assert.True(t, service.IsHashAlgo("sha512"))
|
||||||
|
assert.True(t, service.IsHashAlgo("sha256"))
|
||||||
|
assert.True(t, service.IsHashAlgo("sha1"))
|
||||||
|
assert.True(t, service.IsHashAlgo("md5"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsHashAlgo_Bad(t *testing.T) {
|
||||||
|
assert.False(t, service.IsHashAlgo("not-a-hash"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSA_Bad(t *testing.T) {
|
||||||
|
// Test with a key size that is too small
|
||||||
|
_, _, err := service.GenerateRSAKeyPair(1024)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test decryption with the wrong key
|
||||||
|
pubKey, privKey, err := service.GenerateRSAKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, otherPrivKey, err := service.GenerateRSAKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
message := []byte("secret message")
|
||||||
|
ciphertext, err := service.EncryptRSA(pubKey, message, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = service.DecryptRSA(otherPrivKey, ciphertext, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test decryption with wrong label
|
||||||
|
label1 := []byte("label1")
|
||||||
|
label2 := []byte("label2")
|
||||||
|
ciphertext, err = service.EncryptRSA(pubKey, message, label1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = service.DecryptRSA(privKey, ciphertext, label2)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSA_Ugly(t *testing.T) {
|
||||||
|
// Test with malformed keys
|
||||||
|
_, err := service.EncryptRSA([]byte("not a real key"), []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = service.DecryptRSA([]byte("not a real key"), []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with empty message
|
||||||
|
pubKey, privKey, err := service.GenerateRSAKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
message := []byte("")
|
||||||
|
ciphertext, err := service.EncryptRSA(pubKey, message, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
plaintext, err := service.DecryptRSA(privKey, ciphertext, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, message, plaintext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
170
pkg/crypt/examples_test.go
Normal file
170
pkg/crypt/examples_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
package crypt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleService_Hash() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
payload := "Enchantrix"
|
||||||
|
|
||||||
|
hashTypes := []crypt.HashType{
|
||||||
|
crypt.LTHN,
|
||||||
|
crypt.MD5,
|
||||||
|
crypt.SHA1,
|
||||||
|
crypt.SHA256,
|
||||||
|
crypt.SHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Payload to hash: \"%s\"\n", payload)
|
||||||
|
for _, hashType := range hashTypes {
|
||||||
|
hash := cryptService.Hash(hashType, payload)
|
||||||
|
fmt.Printf(" - %-6s: %s\n", hashType, hash)
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// Payload to hash: "Enchantrix"
|
||||||
|
// - lthn : 331f24f86375846ac8d0d06cfb80cb2877e8900548a88d4ac8d39177cd854dab
|
||||||
|
// - md5 : 7c54903a10f058a93fd1f21ea802cb27
|
||||||
|
// - sha1 : 399f776c4b97e558a2c4f319b223dd481c6d43f1
|
||||||
|
// - sha256: 2ae653f74554abfdb2343013925f5184a0f05e4c2e0c3881448fc80caeb667c2
|
||||||
|
// - sha512: 9638018a9720b5d83fba7f3899e4ba5ab78018781f9c600f0c0738ff8ccf1ea54e1c783ee8778542b70aa26283d87ce88784b2df5697322546d3b8029c4b6797
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_Luhn() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
luhnPayloadGood := "49927398716"
|
||||||
|
luhnPayloadBad := "49927398717"
|
||||||
|
fmt.Printf("Luhn Checksum:\n")
|
||||||
|
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadGood, cryptService.Luhn(luhnPayloadGood))
|
||||||
|
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadBad, cryptService.Luhn(luhnPayloadBad))
|
||||||
|
// Output:
|
||||||
|
// Luhn Checksum:
|
||||||
|
// - Payload '49927398716' is valid: true
|
||||||
|
// - Payload '49927398717' is valid: false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_Fletcher16() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
fletcherPayload := "abcde"
|
||||||
|
fmt.Printf("Fletcher16 Checksum (Payload: \"%s\"): %d\n", fletcherPayload, cryptService.Fletcher16(fletcherPayload))
|
||||||
|
// Output:
|
||||||
|
// Fletcher16 Checksum (Payload: "abcde"): 51440
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_Fletcher32() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
fletcherPayload := "abcde"
|
||||||
|
fmt.Printf("Fletcher32 Checksum (Payload: \"%s\"): %d\n", fletcherPayload, cryptService.Fletcher32(fletcherPayload))
|
||||||
|
// Output:
|
||||||
|
// Fletcher32 Checksum (Payload: "abcde"): 4031760169
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_Fletcher64() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
fletcherPayload := "abcde"
|
||||||
|
fmt.Printf("Fletcher64 Checksum (Payload: \"%s\"): %d\n", fletcherPayload, cryptService.Fletcher64(fletcherPayload))
|
||||||
|
// Output:
|
||||||
|
// Fletcher64 Checksum (Payload: "abcde"): 14467467625952928454
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_GeneratePGPKeyPair() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("PGP public key is not empty: %v\n", len(publicKey) > 0)
|
||||||
|
fmt.Printf("PGP private key is not empty: %v\n", len(privateKey) > 0)
|
||||||
|
// Output:
|
||||||
|
// PGP public key is not empty: true
|
||||||
|
// PGP private key is not empty: true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_EncryptPGP() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
publicKey, _, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
message := []byte("This is a secret message for PGP.")
|
||||||
|
ciphertext, err := cryptService.EncryptPGP(publicKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("PGP ciphertext is not empty: %v\n", len(ciphertext) > 0)
|
||||||
|
// Output:
|
||||||
|
// PGP ciphertext is not empty: true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_DecryptPGP() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
message := []byte("This is a secret message for PGP.")
|
||||||
|
ciphertext, err := cryptService.EncryptPGP(publicKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
decrypted, err := cryptService.DecryptPGP(privateKey, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decrypted message: %s\n", decrypted)
|
||||||
|
// Output:
|
||||||
|
// Decrypted message: This is a secret message for PGP.
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_SignPGP() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
_, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
message := []byte("This is a message to be signed.")
|
||||||
|
signature, err := cryptService.SignPGP(privateKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to sign with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("PGP signature is not empty: %v\n", len(signature) > 0)
|
||||||
|
// Output:
|
||||||
|
// PGP signature is not empty: true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_VerifyPGP() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate PGP key pair: %v", err)
|
||||||
|
}
|
||||||
|
message := []byte("This is a message to be signed.")
|
||||||
|
signature, err := cryptService.SignPGP(privateKey, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to sign with PGP: %v", err)
|
||||||
|
}
|
||||||
|
err = cryptService.VerifyPGP(publicKey, message, signature)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("PGP signature verification failed.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("PGP signature verified successfully.")
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// PGP signature verified successfully.
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleService_SymmetricallyEncryptPGP() {
|
||||||
|
cryptService := crypt.NewService()
|
||||||
|
passphrase := []byte("my secret passphrase")
|
||||||
|
message := []byte("This is a symmetric secret.")
|
||||||
|
ciphertext, err := cryptService.SymmetricallyEncryptPGP(passphrase, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to symmetrically encrypt with PGP: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Symmetric PGP ciphertext is not empty: %v\n", len(ciphertext) > 0)
|
||||||
|
// Output:
|
||||||
|
// Symmetric PGP ciphertext is not empty: true
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
package chachapoly
|
package chachapoly
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mockReader is a reader that returns an error.
|
||||||
|
type mockReader struct{}
|
||||||
|
|
||||||
|
func (r *mockReader) Read(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("read error")
|
||||||
|
}
|
||||||
|
|
||||||
func TestEncryptDecrypt(t *testing.T) {
|
func TestEncryptDecrypt(t *testing.T) {
|
||||||
key := make([]byte, 32)
|
key := make([]byte, 32)
|
||||||
for i := range key {
|
for i := range key {
|
||||||
|
|
@ -83,3 +92,23 @@ func TestCiphertextDiffersFromPlaintext(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEqual(t, plaintext, ciphertext)
|
assert.NotEqual(t, plaintext, ciphertext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncryptNonceError(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
plaintext := []byte("test")
|
||||||
|
|
||||||
|
// Replace the rand.Reader with our mock reader
|
||||||
|
oldReader := rand.Reader
|
||||||
|
rand.Reader = &mockReader{}
|
||||||
|
defer func() { rand.Reader = oldReader }()
|
||||||
|
|
||||||
|
_, err := Encrypt(plaintext, key)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptInvalidKeySize(t *testing.T) {
|
||||||
|
key := make([]byte, 16) // Wrong size
|
||||||
|
ciphertext := []byte("test")
|
||||||
|
_, err := Decrypt(ciphertext, key)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
package lthn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -5,39 +21,53 @@ import (
|
||||||
"encoding/hex"
|
"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{
|
var keyMap = map[rune]rune{
|
||||||
'o': '0',
|
'o': '0', // letter O -> zero
|
||||||
'l': '1',
|
'l': '1', // letter L -> one
|
||||||
'e': '3',
|
'e': '3', // letter E -> three
|
||||||
'a': '4',
|
'a': '4', // letter A -> four
|
||||||
's': 'z',
|
's': 'z', // letter S -> Z
|
||||||
't': '7',
|
't': '7', // letter T -> seven
|
||||||
'0': 'o',
|
'0': 'o', // zero -> letter O
|
||||||
'1': 'l',
|
'1': 'l', // one -> letter L
|
||||||
'3': 'e',
|
'3': 'e', // three -> letter E
|
||||||
'4': 'a',
|
'4': 'a', // four -> letter A
|
||||||
'7': 't',
|
'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) {
|
func SetKeyMap(newKeyMap map[rune]rune) {
|
||||||
keyMap = newKeyMap
|
keyMap = newKeyMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeyMap gets the current key map.
|
// GetKeyMap returns the current character substitution map.
|
||||||
func GetKeyMap() map[rune]rune {
|
func GetKeyMap() map[rune]rune {
|
||||||
return keyMap
|
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 {
|
func Hash(input string) string {
|
||||||
salt := createSalt(input)
|
salt := createSalt(input)
|
||||||
hash := sha256.Sum256([]byte(input + salt))
|
hash := sha256.Sum256([]byte(input + salt))
|
||||||
return hex.EncodeToString(hash[:])
|
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 {
|
func createSalt(input string) string {
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -55,7 +85,10 @@ func createSalt(input string) string {
|
||||||
return string(salt)
|
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 {
|
func Verify(input string, hash string) bool {
|
||||||
return Hash(input) == hash
|
return Hash(input) == hash
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
pkg/crypt/std/lthn/lthn_internal_test.go
Normal file
37
pkg/crypt/std/lthn/lthn_internal_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package lthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateSalt_Good(t *testing.T) {
|
||||||
|
// "hello" reversed: "olleh" -> "0113h"
|
||||||
|
expected := "0113h"
|
||||||
|
actual := createSalt("hello")
|
||||||
|
assert.Equal(t, expected, actual, "Salt should be correctly created for 'hello'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSalt_Bad(t *testing.T) {
|
||||||
|
// Test with an empty string
|
||||||
|
expected := ""
|
||||||
|
actual := createSalt("")
|
||||||
|
assert.Equal(t, expected, actual, "Salt for an empty string should be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSalt_Ugly(t *testing.T) {
|
||||||
|
// Test with characters not in the keyMap
|
||||||
|
input := "world123"
|
||||||
|
// "world123" reversed: "321dlrow" -> "e2ld1r0w"
|
||||||
|
expected := "e2ld1r0w"
|
||||||
|
actual := createSalt(input)
|
||||||
|
assert.Equal(t, expected, actual, "Salt should handle characters not in the keyMap")
|
||||||
|
|
||||||
|
// Test with only characters in the keyMap
|
||||||
|
input = "oleta"
|
||||||
|
// "oleta" reversed: "atelo" -> "47310"
|
||||||
|
expected = "47310"
|
||||||
|
actual = createSalt(input)
|
||||||
|
assert.Equal(t, expected, actual, "Salt should correctly handle strings with only keyMap characters")
|
||||||
|
}
|
||||||
25
pkg/crypt/std/lthn/lthn_keymap_test.go
Normal file
25
pkg/crypt/std/lthn/lthn_keymap_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package lthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testKeyMapMu sync.Mutex
|
||||||
|
|
||||||
|
func TestSetKeyMap(t *testing.T) {
|
||||||
|
testKeyMapMu.Lock()
|
||||||
|
originalKeyMap := GetKeyMap()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
SetKeyMap(originalKeyMap)
|
||||||
|
testKeyMapMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
newKeyMap := map[rune]rune{
|
||||||
|
'a': 'b',
|
||||||
|
}
|
||||||
|
SetKeyMap(newKeyMap)
|
||||||
|
assert.Equal(t, newKeyMap, GetKeyMap())
|
||||||
|
}
|
||||||
206
pkg/crypt/std/pgp/pgp.go
Normal file
206
pkg/crypt/std/pgp/pgp.go
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
|
||||||
|
package pgp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service is a service for PGP operations.
|
||||||
|
type Service struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
openpgpNewEntity = openpgp.NewEntity
|
||||||
|
openpgpReadArmoredKeyRing = openpgp.ReadArmoredKeyRing
|
||||||
|
openpgpEncrypt = openpgp.Encrypt
|
||||||
|
openpgpReadMessage = openpgp.ReadMessage
|
||||||
|
openpgpArmoredDetachSign = openpgp.ArmoredDetachSign
|
||||||
|
openpgpCheckArmoredDetachedSignature = openpgp.CheckArmoredDetachedSignature
|
||||||
|
openpgpSymmetricallyEncrypt = openpgp.SymmetricallyEncrypt
|
||||||
|
armorEncode = armor.Encode
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewService creates a new PGP Service.
|
||||||
|
func NewService() *Service {
|
||||||
|
return &Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeyPair generates a new PGP key pair.
|
||||||
|
func (s *Service) GenerateKeyPair(name, email, comment string) (publicKey, privateKey []byte, err error) {
|
||||||
|
entity, err := openpgpNewEntity(name, comment, email, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create new entity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign all the identities
|
||||||
|
for _, id := range entity.Identities {
|
||||||
|
_ = id.SelfSignature.SignUserId(id.UserId.Id, entity.PrimaryKey, entity.PrivateKey, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Key
|
||||||
|
pubKeyBuf := new(bytes.Buffer)
|
||||||
|
pubKeyWriter, err := armorEncode(pubKeyBuf, openpgp.PublicKeyType, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create armored public key writer: %w", err)
|
||||||
|
}
|
||||||
|
defer pubKeyWriter.Close()
|
||||||
|
if err := entity.Serialize(pubKeyWriter); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to serialize public key: %w", err)
|
||||||
|
}
|
||||||
|
// a tricky little bastard, this one. without closing the writer, the buffer is empty.
|
||||||
|
pubKeyWriter.Close()
|
||||||
|
|
||||||
|
// Private Key
|
||||||
|
privKeyBuf := new(bytes.Buffer)
|
||||||
|
privKeyWriter, err := armorEncode(privKeyBuf, openpgp.PrivateKeyType, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create armored private key writer: %w", err)
|
||||||
|
}
|
||||||
|
defer privKeyWriter.Close()
|
||||||
|
if err := entity.SerializePrivate(privKeyWriter, nil); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to serialize private key: %w", err)
|
||||||
|
}
|
||||||
|
// a tricky little bastard, this one. without closing the writer, the buffer is empty.
|
||||||
|
privKeyWriter.Close()
|
||||||
|
|
||||||
|
return pubKeyBuf.Bytes(), privKeyBuf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts data with a public key.
|
||||||
|
func (s *Service) Encrypt(publicKey, data []byte) ([]byte, error) {
|
||||||
|
pubKeyReader := bytes.NewReader(publicKey)
|
||||||
|
keyring, err := openpgpReadArmoredKeyRing(pubKeyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read public key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
w, err := openpgpEncrypt(buf, keyring, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create encryption writer: %w", err)
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
_, err = w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write data to encryption writer: %w", err)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data with a private key.
|
||||||
|
func (s *Service) Decrypt(privateKey, ciphertext []byte) ([]byte, error) {
|
||||||
|
privKeyReader := bytes.NewReader(privateKey)
|
||||||
|
keyring, err := openpgpReadArmoredKeyRing(privKeyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read private key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewReader(ciphertext)
|
||||||
|
md, err := openpgpReadMessage(buf, keyring, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := io.ReadAll(md.UnverifiedBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read plaintext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign creates a detached signature for a message.
|
||||||
|
func (s *Service) Sign(privateKey, data []byte) ([]byte, error) {
|
||||||
|
privKeyReader := bytes.NewReader(privateKey)
|
||||||
|
keyring, err := openpgpReadArmoredKeyRing(privKeyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read private key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := keyring[0]
|
||||||
|
if signer.PrivateKey == nil {
|
||||||
|
return nil, fmt.Errorf("private key not found in keyring")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err = openpgpArmoredDetachSign(buf, signer, bytes.NewReader(data), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies a detached signature for a message.
|
||||||
|
func (s *Service) Verify(publicKey, data, signature []byte) error {
|
||||||
|
pubKeyReader := bytes.NewReader(publicKey)
|
||||||
|
keyring, err := openpgpReadArmoredKeyRing(pubKeyReader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read public key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = openpgpCheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(signature), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to verify signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SymmetricallyEncrypt encrypts data with a passphrase.
|
||||||
|
func (s *Service) SymmetricallyEncrypt(passphrase, data []byte) ([]byte, error) {
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
return nil, fmt.Errorf("passphrase cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
w, err := openpgpSymmetricallyEncrypt(buf, passphrase, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create symmetric encryption writer: %w", err)
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
_, err = w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write data to symmetric encryption writer: %w", err)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SymmetricallyDecrypt decrypts data with a passphrase.
|
||||||
|
func (s *Service) SymmetricallyDecrypt(passphrase, ciphertext []byte) ([]byte, error) {
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
return nil, fmt.Errorf("passphrase cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewReader(ciphertext)
|
||||||
|
failed := false
|
||||||
|
prompt := func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
|
||||||
|
if failed {
|
||||||
|
return nil, fmt.Errorf("decryption failed")
|
||||||
|
}
|
||||||
|
failed = true
|
||||||
|
return passphrase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := openpgpReadMessage(buf, nil, prompt, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := io.ReadAll(md.UnverifiedBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read plaintext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
417
pkg/crypt/std/pgp/pgp_test.go
Normal file
417
pkg/crypt/std/pgp/pgp_test.go
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
|
||||||
|
package pgp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_GenerateKeyPair_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err, "failed to generate key pair")
|
||||||
|
assert.NotNil(t, pub, "public key is nil")
|
||||||
|
assert.NotNil(t, priv, "private key is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_GenerateKeyPair_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
// Test with invalid name (null byte)
|
||||||
|
_, _, err := s.GenerateKeyPair("test\x00", "test@test.com", "test")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Encrypt_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
pub, _, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err, "failed to generate key pair")
|
||||||
|
assert.NotNil(t, pub, "public key is nil")
|
||||||
|
|
||||||
|
data := []byte("hello world")
|
||||||
|
encrypted, err := s.Encrypt(pub, data)
|
||||||
|
require.NoError(t, err, "failed to encrypt data")
|
||||||
|
assert.NotNil(t, encrypted, "encrypted data is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_SymmetricallyEncrypt_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
// Test with empty passphrase
|
||||||
|
_, err := s.SymmetricallyEncrypt([]byte(""), []byte("hello world"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_SymmetricallyDecrypt_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
passphrase := []byte("hello world")
|
||||||
|
data := []byte("hello world")
|
||||||
|
encrypted, err := s.SymmetricallyEncrypt(passphrase, data)
|
||||||
|
require.NoError(t, err, "failed to encrypt data")
|
||||||
|
assert.NotNil(t, encrypted, "encrypted data is nil")
|
||||||
|
|
||||||
|
decrypted, err := s.SymmetricallyDecrypt(passphrase, encrypted)
|
||||||
|
require.NoError(t, err, "failed to decrypt data")
|
||||||
|
assert.Equal(t, data, decrypted, "decrypted data does not match original")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_SymmetricallyDecrypt_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
// Test with empty passphrase
|
||||||
|
_, err := s.SymmetricallyDecrypt([]byte(""), []byte("hello world"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with wrong passphrase
|
||||||
|
passphrase := []byte("hello world")
|
||||||
|
data := []byte("hello world")
|
||||||
|
encrypted, err := s.SymmetricallyEncrypt(passphrase, data)
|
||||||
|
require.NoError(t, err, "failed to encrypt data")
|
||||||
|
|
||||||
|
_, err = s.SymmetricallyDecrypt([]byte("wrong passphrase"), encrypted)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with bad encrypted data
|
||||||
|
_, err = s.SymmetricallyDecrypt(passphrase, []byte("bad encrypted data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with corrupt body
|
||||||
|
pub3, priv3, err := s.GenerateKeyPair("test3", "test3@test.com", "test3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
encrypted3, err := s.Encrypt(pub3, []byte("hello world"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
encrypted3[len(encrypted3)-1] ^= 0x01
|
||||||
|
_, err = s.Decrypt(priv3, encrypted3)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Encrypt_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
_, err := s.Encrypt([]byte("bad key"), []byte("hello world"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Decrypt_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err, "failed to generate key pair")
|
||||||
|
assert.NotNil(t, pub, "public key is nil")
|
||||||
|
assert.NotNil(t, priv, "private key is nil")
|
||||||
|
|
||||||
|
data := []byte("hello world")
|
||||||
|
encrypted, err := s.Encrypt(pub, data)
|
||||||
|
require.NoError(t, err, "failed to encrypt data")
|
||||||
|
assert.NotNil(t, encrypted, "encrypted data is nil")
|
||||||
|
|
||||||
|
decrypted, err := s.Decrypt(priv, encrypted)
|
||||||
|
require.NoError(t, err, "failed to decrypt data")
|
||||||
|
assert.Equal(t, data, decrypted, "decrypted data does not match original")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Decrypt_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
_, err := s.Decrypt([]byte("bad key"), []byte("hello world"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
pub, _, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, priv2, err := s.GenerateKeyPair("test2", "test2@test.com", "test2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
encrypted, err := s.Encrypt(pub, []byte("hello world"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = s.Decrypt(priv2, encrypted)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = s.Decrypt(priv2, []byte("bad encrypted data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with corrupt body
|
||||||
|
pub3, priv3, err := s.GenerateKeyPair("test3", "test3@test.com", "test3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
encrypted3, err := s.Encrypt(pub3, []byte("hello world"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
encrypted3[len(encrypted3)-1] ^= 0x01
|
||||||
|
_, err = s.Decrypt(priv3, encrypted3)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Sign_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
_, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err, "failed to generate key pair")
|
||||||
|
assert.NotNil(t, priv, "private key is nil")
|
||||||
|
|
||||||
|
data := []byte("hello world")
|
||||||
|
signature, err := s.Sign(priv, data)
|
||||||
|
require.NoError(t, err, "failed to sign data")
|
||||||
|
assert.NotNil(t, signature, "signature is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Sign_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
_, err := s.Sign([]byte("bad key"), []byte("hello world"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with public key (no private key)
|
||||||
|
pub, _, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = s.Sign(pub, []byte("hello world"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "private key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Verify_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err, "failed to generate key pair")
|
||||||
|
assert.NotNil(t, pub, "public key is nil")
|
||||||
|
assert.NotNil(t, priv, "private key is nil")
|
||||||
|
|
||||||
|
data := []byte("hello world")
|
||||||
|
signature, err := s.Sign(priv, data)
|
||||||
|
require.NoError(t, err, "failed to sign data")
|
||||||
|
assert.NotNil(t, signature, "signature is nil")
|
||||||
|
|
||||||
|
err = s.Verify(pub, data, signature)
|
||||||
|
require.NoError(t, err, "failed to verify signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Verify_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
err := s.Verify([]byte("bad key"), []byte("hello world"), []byte("bad signature"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
pub2, _, err := s.GenerateKeyPair("test2", "test2@test.com", "test2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
signature, err := s.Sign(priv, []byte("hello world"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = s.Verify(pub2, []byte("hello world"), signature)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_SymmetricallyEncrypt_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
passphrase := []byte("hello world")
|
||||||
|
data := []byte("hello world")
|
||||||
|
encrypted, err := s.SymmetricallyEncrypt(passphrase, data)
|
||||||
|
require.NoError(t, err, "failed to encrypt data")
|
||||||
|
assert.NotNil(t, encrypted, "encrypted data is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock testing infrastructure
|
||||||
|
|
||||||
|
type mockWriteCloser struct {
|
||||||
|
writeFunc func(p []byte) (n int, err error)
|
||||||
|
closeFunc func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWriteCloser) Write(p []byte) (n int, err error) {
|
||||||
|
if m.writeFunc != nil {
|
||||||
|
return m.writeFunc(p)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWriteCloser) Close() error {
|
||||||
|
if m.closeFunc != nil {
|
||||||
|
return m.closeFunc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockReader struct {
|
||||||
|
readFunc func(p []byte) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReader) Read(p []byte) (n int, err error) {
|
||||||
|
if m.readFunc != nil {
|
||||||
|
return m.readFunc(p)
|
||||||
|
}
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_GenerateKeyPair_MockErrors(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
origNewEntity := openpgpNewEntity
|
||||||
|
origArmorEncode := armorEncode
|
||||||
|
defer func() {
|
||||||
|
openpgpNewEntity = origNewEntity
|
||||||
|
armorEncode = origArmorEncode
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 1. Mock NewEntity error
|
||||||
|
openpgpNewEntity = func(name, comment, email string, config *packet.Config) (*openpgp.Entity, error) {
|
||||||
|
return nil, errors.New("mock new entity error")
|
||||||
|
}
|
||||||
|
_, _, err := s.GenerateKeyPair("test", "test", "test")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock new entity error")
|
||||||
|
openpgpNewEntity = origNewEntity // restore
|
||||||
|
|
||||||
|
// 2. Mock armorEncode error (public key)
|
||||||
|
armorEncode = func(out io.Writer, typeStr string, headers map[string]string) (io.WriteCloser, error) {
|
||||||
|
if typeStr == openpgp.PublicKeyType {
|
||||||
|
return nil, errors.New("mock armor pub error")
|
||||||
|
}
|
||||||
|
return origArmorEncode(out, typeStr, headers)
|
||||||
|
}
|
||||||
|
_, _, err = s.GenerateKeyPair("test", "test", "test")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock armor pub error")
|
||||||
|
armorEncode = origArmorEncode // restore
|
||||||
|
|
||||||
|
// 3. Mock armorEncode error (private key)
|
||||||
|
armorEncode = func(out io.Writer, typeStr string, headers map[string]string) (io.WriteCloser, error) {
|
||||||
|
if typeStr == openpgp.PrivateKeyType {
|
||||||
|
return nil, errors.New("mock armor priv error")
|
||||||
|
}
|
||||||
|
return origArmorEncode(out, typeStr, headers)
|
||||||
|
}
|
||||||
|
_, _, err = s.GenerateKeyPair("test", "test", "test")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock armor priv error")
|
||||||
|
armorEncode = origArmorEncode // restore
|
||||||
|
|
||||||
|
// 4. Mock Serialize error (via Write failure)
|
||||||
|
// We need armorEncode to return a writer that fails on Write
|
||||||
|
armorEncode = func(out io.Writer, typeStr string, headers map[string]string) (io.WriteCloser, error) {
|
||||||
|
if typeStr == openpgp.PublicKeyType {
|
||||||
|
return &mockWriteCloser{
|
||||||
|
writeFunc: func(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("mock write pub error")
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return origArmorEncode(out, typeStr, headers)
|
||||||
|
}
|
||||||
|
_, _, err = s.GenerateKeyPair("test", "test", "test")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock write pub error")
|
||||||
|
armorEncode = origArmorEncode // restore
|
||||||
|
|
||||||
|
// 5. Mock SerializePrivate error (via Write failure)
|
||||||
|
armorEncode = func(out io.Writer, typeStr string, headers map[string]string) (io.WriteCloser, error) {
|
||||||
|
if typeStr == openpgp.PrivateKeyType {
|
||||||
|
return &mockWriteCloser{
|
||||||
|
writeFunc: func(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("mock write priv error")
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return origArmorEncode(out, typeStr, headers)
|
||||||
|
}
|
||||||
|
_, _, err = s.GenerateKeyPair("test", "test", "test")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock write priv error")
|
||||||
|
armorEncode = origArmorEncode // restore
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestService_Encrypt_MockErrors(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
pub, _, err := s.GenerateKeyPair("test", "test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
origEncrypt := openpgpEncrypt
|
||||||
|
defer func() { openpgpEncrypt = origEncrypt }()
|
||||||
|
|
||||||
|
// 1. Mock Encrypt error
|
||||||
|
openpgpEncrypt = func(ciphertext io.Writer, to []*openpgp.Entity, signed *openpgp.Entity, hints *openpgp.FileHints, config *packet.Config) (io.WriteCloser, error) {
|
||||||
|
return nil, errors.New("mock encrypt error")
|
||||||
|
}
|
||||||
|
_, err = s.Encrypt(pub, []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock encrypt error")
|
||||||
|
|
||||||
|
// 2. Mock Write error
|
||||||
|
openpgpEncrypt = func(ciphertext io.Writer, to []*openpgp.Entity, signed *openpgp.Entity, hints *openpgp.FileHints, config *packet.Config) (io.WriteCloser, error) {
|
||||||
|
return &mockWriteCloser{
|
||||||
|
writeFunc: func(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("mock write data error")
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
_, err = s.Encrypt(pub, []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock write data error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Sign_MockErrors(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
_, priv, err := s.GenerateKeyPair("test", "test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
origSign := openpgpArmoredDetachSign
|
||||||
|
defer func() { openpgpArmoredDetachSign = origSign }()
|
||||||
|
|
||||||
|
// Mock Sign error
|
||||||
|
openpgpArmoredDetachSign = func(w io.Writer, signer *openpgp.Entity, message io.Reader, config *packet.Config) error {
|
||||||
|
return errors.New("mock sign error")
|
||||||
|
}
|
||||||
|
_, err = s.Sign(priv, []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock sign error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_SymmetricallyEncrypt_MockErrors(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
|
||||||
|
origSymEncrypt := openpgpSymmetricallyEncrypt
|
||||||
|
defer func() { openpgpSymmetricallyEncrypt = origSymEncrypt }()
|
||||||
|
|
||||||
|
// 1. Mock Sym Encrypt error
|
||||||
|
openpgpSymmetricallyEncrypt = func(ciphertext io.Writer, passphrase []byte, hints *openpgp.FileHints, config *packet.Config) (io.WriteCloser, error) {
|
||||||
|
return nil, errors.New("mock sym encrypt error")
|
||||||
|
}
|
||||||
|
_, err := s.SymmetricallyEncrypt([]byte("pass"), []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock sym encrypt error")
|
||||||
|
|
||||||
|
// 2. Mock Write error
|
||||||
|
openpgpSymmetricallyEncrypt = func(ciphertext io.Writer, passphrase []byte, hints *openpgp.FileHints, config *packet.Config) (io.WriteCloser, error) {
|
||||||
|
return &mockWriteCloser{
|
||||||
|
writeFunc: func(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("mock sym write error")
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
_, err = s.SymmetricallyEncrypt([]byte("pass"), []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock sym write error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_SymmetricallyDecrypt_MockErrors(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
pass := []byte("pass")
|
||||||
|
|
||||||
|
origReadMessage := openpgpReadMessage
|
||||||
|
defer func() { openpgpReadMessage = origReadMessage }()
|
||||||
|
|
||||||
|
// Mock ReadMessage error
|
||||||
|
openpgpReadMessage = func(r io.Reader, keyring openpgp.KeyRing, prompt openpgp.PromptFunction, config *packet.Config) (*openpgp.MessageDetails, error) {
|
||||||
|
return nil, errors.New("mock read message error")
|
||||||
|
}
|
||||||
|
_, err := s.SymmetricallyDecrypt(pass, []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock read message error")
|
||||||
|
|
||||||
|
// Mock ReadAll error (via ReadMessage returning bad body)
|
||||||
|
openpgpReadMessage = func(r io.Reader, keyring openpgp.KeyRing, prompt openpgp.PromptFunction, config *packet.Config) (*openpgp.MessageDetails, error) {
|
||||||
|
// We need to return a message with UnverifiedBody that fails on Read
|
||||||
|
return &openpgp.MessageDetails{
|
||||||
|
UnverifiedBody: &mockReader{
|
||||||
|
readFunc: func(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("mock read body error")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
_, err = s.SymmetricallyDecrypt(pass, []byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "mock read body error")
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,91 @@
|
||||||
package rsa
|
package rsa
|
||||||
|
|
||||||
// This file is a placeholder for RSA key handling functionality.
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service provides RSA functionality.
|
||||||
|
type Service struct{}
|
||||||
|
|
||||||
|
// NewService creates and returns a new Service instance for performing RSA-related operations.
|
||||||
|
func NewService() *Service {
|
||||||
|
return &Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeyPair creates a new RSA key pair.
|
||||||
|
func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) {
|
||||||
|
if bits < 2048 {
|
||||||
|
return nil, nil, fmt.Errorf("rsa: key size too small: %d (minimum 2048)", bits)
|
||||||
|
}
|
||||||
|
privKey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privKeyBytes := x509.MarshalPKCS1PrivateKey(privKey)
|
||||||
|
privKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: privKeyBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to marshal public key: %w", err)
|
||||||
|
}
|
||||||
|
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: pubKeyBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
return pubKeyPEM, privKeyPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts data with a public key.
|
||||||
|
func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) {
|
||||||
|
block, _ := pem.Decode(publicKey)
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsaPub, ok := pub.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("not an RSA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, data, label)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encrypt data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data with a private key.
|
||||||
|
func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) {
|
||||||
|
block, _ := pem.Decode(privateKey)
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ciphertext, label)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decrypt data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
101
pkg/crypt/std/rsa/rsa_test.go
Normal file
101
pkg/crypt/std/rsa/rsa_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
package rsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockReader is a reader that returns an error.
|
||||||
|
type mockReader struct{}
|
||||||
|
|
||||||
|
func (r *mockReader) Read(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("read error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSA_Good(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
|
||||||
|
// Generate a new key pair
|
||||||
|
pubKey, privKey, err := s.GenerateKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, pubKey)
|
||||||
|
assert.NotEmpty(t, privKey)
|
||||||
|
|
||||||
|
// Encrypt and decrypt a message
|
||||||
|
message := []byte("Hello, World!")
|
||||||
|
ciphertext, err := s.Encrypt(pubKey, message, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
plaintext, err := s.Decrypt(privKey, ciphertext, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, message, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSA_Bad(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
|
||||||
|
// Decrypt with wrong key
|
||||||
|
pubKey, _, err := s.GenerateKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, otherPrivKey, err := s.GenerateKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
message := []byte("Hello, World!")
|
||||||
|
ciphertext, err := s.Encrypt(pubKey, message, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = s.Decrypt(otherPrivKey, ciphertext, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Key size too small
|
||||||
|
_, _, err = s.GenerateKeyPair(512)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSA_Ugly(t *testing.T) {
|
||||||
|
s := NewService()
|
||||||
|
|
||||||
|
// Malformed keys and messages
|
||||||
|
_, err := s.Encrypt([]byte("not-a-key"), []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
_, err = s.Decrypt([]byte("not-a-key"), []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
_, err = s.Encrypt([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ/6j/y7/r/9/z/8/f/+/v7+/v7+/v7+\nv/7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v4=\n-----END PUBLIC KEY-----"), []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
_, err = s.Decrypt([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAL/6j/y7/r/9/z/8/f/+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+\nv/7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v4CAwEAAQJB\nAL/6j/y7/r/9/z/8/f/+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+\nv/7+/v7+/v7+/v7+/v7+/v7+/v7+/v4CgYEA/f8/vLv+v/3/P/z9//7+/v7+/v7+\nvv7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v4C\ngYEA/f8/vLv+v/3/P/z9//7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+\nvv7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v4CgYEA/f8/vLv+v/3/P/z9//7+/v7+\nvv7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+\nv/4CgYEA/f8/vLv+v/3/P/z9//7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+\nvv7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v4CgYEA/f8/vLv+v/3/P/z9//7+/v7+\nvv7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+\nv/4=\n-----END RSA PRIVATE KEY-----"), []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Key generation failure
|
||||||
|
oldReader := rand.Reader
|
||||||
|
rand.Reader = &mockReader{}
|
||||||
|
t.Cleanup(func() { rand.Reader = oldReader })
|
||||||
|
_, _, err = s.GenerateKeyPair(2048)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Encrypt with non-RSA key
|
||||||
|
rand.Reader = oldReader // Restore reader for this test
|
||||||
|
ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
ecdsaPubKeyBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPrivKey.PublicKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
ecdsaPubKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: ecdsaPubKeyBytes,
|
||||||
|
})
|
||||||
|
_, err = s.Encrypt(ecdsaPubKeyPEM, []byte("message"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
rand.Reader = &mockReader{} // Set it back for the next test
|
||||||
|
|
||||||
|
// Encrypt message too long
|
||||||
|
rand.Reader = oldReader // Restore reader for this test
|
||||||
|
pubKey, _, err := s.GenerateKeyPair(2048)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
message := make([]byte, 2048)
|
||||||
|
_, err = s.Encrypt(pubKey, message, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
rand.Reader = &mockReader{} // Set it back
|
||||||
|
}
|
||||||
372
pkg/enchantrix/crypto_sigil.go
Normal file
372
pkg/enchantrix/crypto_sigil.go
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
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"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidKey is returned when the encryption key is invalid.
|
||||||
|
ErrInvalidKey = errors.New("enchantrix: invalid key size, must be 32 bytes")
|
||||||
|
// ErrCiphertextTooShort is returned when the ciphertext is too short to decrypt.
|
||||||
|
ErrCiphertextTooShort = errors.New("enchantrix: ciphertext too short")
|
||||||
|
// ErrDecryptionFailed is returned when decryption or authentication fails.
|
||||||
|
ErrDecryptionFailed = errors.New("enchantrix: decryption failed")
|
||||||
|
// ErrNoKeyConfigured is returned when no encryption key has been set.
|
||||||
|
ErrNoKeyConfigured = errors.New("enchantrix: no encryption key configured")
|
||||||
|
)
|
||||||
|
|
||||||
|
// PreObfuscator applies a reversible transformation to data before encryption.
|
||||||
|
// 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 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 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.
|
||||||
|
func (x *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return x.transform(data, entropy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deobfuscate reverses the XOR transformation (XOR is symmetric).
|
||||||
|
func (x *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return x.transform(data, entropy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform applies XOR with an entropy-derived key stream.
|
||||||
|
func (x *XORObfuscator) transform(data []byte, entropy []byte) []byte {
|
||||||
|
result := make([]byte, len(data))
|
||||||
|
keyStream := x.deriveKeyStream(entropy, len(data))
|
||||||
|
for i := range data {
|
||||||
|
result[i] = data[i] ^ keyStream[i]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveKeyStream creates a deterministic key stream from entropy.
|
||||||
|
func (x *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
|
||||||
|
stream := make([]byte, length)
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
// Generate key stream in 32-byte blocks
|
||||||
|
blockNum := uint64(0)
|
||||||
|
offset := 0
|
||||||
|
for offset < length {
|
||||||
|
h.Reset()
|
||||||
|
h.Write(entropy)
|
||||||
|
var blockBytes [8]byte
|
||||||
|
binary.BigEndian.PutUint64(blockBytes[:], blockNum)
|
||||||
|
h.Write(blockBytes[:])
|
||||||
|
block := h.Sum(nil)
|
||||||
|
|
||||||
|
copyLen := len(block)
|
||||||
|
if offset+copyLen > length {
|
||||||
|
copyLen = length - offset
|
||||||
|
}
|
||||||
|
copy(stream[offset:], block[:copyLen])
|
||||||
|
offset += copyLen
|
||||||
|
blockNum++
|
||||||
|
}
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (s *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, len(data))
|
||||||
|
copy(result, data)
|
||||||
|
|
||||||
|
// Generate permutation and mask from entropy
|
||||||
|
perm := s.generatePermutation(entropy, len(data))
|
||||||
|
mask := s.deriveMask(entropy, len(data))
|
||||||
|
|
||||||
|
// Apply mask first, then shuffle
|
||||||
|
for i := range result {
|
||||||
|
result[i] ^= mask[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle using Fisher-Yates with deterministic seed
|
||||||
|
shuffled := make([]byte, len(data))
|
||||||
|
for i, p := range perm {
|
||||||
|
shuffled[i] = result[p]
|
||||||
|
}
|
||||||
|
|
||||||
|
return shuffled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deobfuscate reverses the shuffle and mask operations.
|
||||||
|
func (s *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, len(data))
|
||||||
|
|
||||||
|
// Generate permutation and mask from entropy
|
||||||
|
perm := s.generatePermutation(entropy, len(data))
|
||||||
|
mask := s.deriveMask(entropy, len(data))
|
||||||
|
|
||||||
|
// Unshuffle first
|
||||||
|
for i, p := range perm {
|
||||||
|
result[p] = data[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove mask
|
||||||
|
for i := range result {
|
||||||
|
result[i] ^= mask[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePermutation creates a deterministic permutation from entropy.
|
||||||
|
func (s *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int) []int {
|
||||||
|
perm := make([]int, length)
|
||||||
|
for i := range perm {
|
||||||
|
perm[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use entropy to seed a deterministic shuffle
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(entropy)
|
||||||
|
h.Write([]byte("permutation"))
|
||||||
|
seed := h.Sum(nil)
|
||||||
|
|
||||||
|
// Fisher-Yates shuffle with deterministic randomness
|
||||||
|
for i := length - 1; i > 0; i-- {
|
||||||
|
h.Reset()
|
||||||
|
h.Write(seed)
|
||||||
|
var iBytes [8]byte
|
||||||
|
binary.BigEndian.PutUint64(iBytes[:], uint64(i))
|
||||||
|
h.Write(iBytes[:])
|
||||||
|
jBytes := h.Sum(nil)
|
||||||
|
j := int(binary.BigEndian.Uint64(jBytes[:8]) % uint64(i+1))
|
||||||
|
perm[i], perm[j] = perm[j], perm[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return perm
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveMask creates a mask byte array from entropy.
|
||||||
|
func (s *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
|
||||||
|
mask := make([]byte, length)
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
blockNum := uint64(0)
|
||||||
|
offset := 0
|
||||||
|
for offset < length {
|
||||||
|
h.Reset()
|
||||||
|
h.Write(entropy)
|
||||||
|
h.Write([]byte("mask"))
|
||||||
|
var blockBytes [8]byte
|
||||||
|
binary.BigEndian.PutUint64(blockBytes[:], blockNum)
|
||||||
|
h.Write(blockBytes[:])
|
||||||
|
block := h.Sum(nil)
|
||||||
|
|
||||||
|
copyLen := len(block)
|
||||||
|
if offset+copyLen > length {
|
||||||
|
copyLen = length - offset
|
||||||
|
}
|
||||||
|
copy(mask[offset:], block[:copyLen])
|
||||||
|
offset += copyLen
|
||||||
|
blockNum++
|
||||||
|
}
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChaChaPolySigil is a Sigil that encrypts/decrypts data using ChaCha20-Poly1305.
|
||||||
|
// It applies pre-obfuscation before encryption to ensure raw plaintext never
|
||||||
|
// goes directly to CPU encryption routines.
|
||||||
|
//
|
||||||
|
// The output format is:
|
||||||
|
// [24-byte nonce][encrypted(obfuscated(plaintext))]
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChaChaPolySigil creates a new encryption sigil with the given key.
|
||||||
|
// The key must be exactly 32 bytes.
|
||||||
|
func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) {
|
||||||
|
if len(key) != 32 {
|
||||||
|
return nil, ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCopy := make([]byte, 32)
|
||||||
|
copy(keyCopy, key)
|
||||||
|
|
||||||
|
return &ChaChaPolySigil{
|
||||||
|
Key: keyCopy,
|
||||||
|
Obfuscator: &XORObfuscator{},
|
||||||
|
randReader: rand.Reader,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChaChaPolySigilWithObfuscator creates a new encryption sigil with custom obfuscator.
|
||||||
|
func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil, error) {
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if obfuscator != nil {
|
||||||
|
sigil.Obfuscator = obfuscator
|
||||||
|
}
|
||||||
|
return sigil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// In encrypts the data with pre-obfuscation.
|
||||||
|
// The flow is: plaintext -> obfuscate -> encrypt
|
||||||
|
func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
||||||
|
if s.Key == nil {
|
||||||
|
return nil, ErrNoKeyConfigured
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := chacha20poly1305.NewX(s.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate nonce
|
||||||
|
nonce := make([]byte, aead.NonceSize())
|
||||||
|
reader := s.randReader
|
||||||
|
if reader == nil {
|
||||||
|
reader = rand.Reader
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(reader, nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-obfuscate the plaintext using nonce as entropy
|
||||||
|
// This ensures CPU encryption routines never see raw plaintext
|
||||||
|
obfuscated := data
|
||||||
|
if s.Obfuscator != nil {
|
||||||
|
obfuscated = s.Obfuscator.Obfuscate(data, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the obfuscated data
|
||||||
|
// Output: [nonce | ciphertext | auth tag]
|
||||||
|
ciphertext := aead.Seal(nonce, nonce, obfuscated, nil)
|
||||||
|
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decrypts the data and reverses obfuscation.
|
||||||
|
// The flow is: decrypt -> deobfuscate -> plaintext
|
||||||
|
func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if s.Key == nil {
|
||||||
|
return nil, ErrNoKeyConfigured
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := chacha20poly1305.NewX(s.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
minLen := aead.NonceSize() + aead.Overhead()
|
||||||
|
if len(data) < minLen {
|
||||||
|
return nil, ErrCiphertextTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract nonce from ciphertext
|
||||||
|
nonce := data[:aead.NonceSize()]
|
||||||
|
ciphertext := data[aead.NonceSize():]
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
obfuscated, err := aead.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrDecryptionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deobfuscate using the same nonce as entropy
|
||||||
|
plaintext := obfuscated
|
||||||
|
if s.Obfuscator != nil {
|
||||||
|
plaintext = s.Obfuscator.Deobfuscate(obfuscated, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plaintext) == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNonceFromCiphertext extracts the nonce from encrypted output.
|
||||||
|
// This is provided for debugging/logging purposes only.
|
||||||
|
// The nonce should NOT be stored separately in headers.
|
||||||
|
func GetNonceFromCiphertext(ciphertext []byte) ([]byte, error) {
|
||||||
|
nonceSize := chacha20poly1305.NonceSizeX
|
||||||
|
if len(ciphertext) < nonceSize {
|
||||||
|
return nil, ErrCiphertextTooShort
|
||||||
|
}
|
||||||
|
nonceCopy := make([]byte, nonceSize)
|
||||||
|
copy(nonceCopy, ciphertext[:nonceSize])
|
||||||
|
return nonceCopy, nil
|
||||||
|
}
|
||||||
524
pkg/enchantrix/crypto_sigil_test.go
Normal file
524
pkg/enchantrix/crypto_sigil_test.go
Normal file
|
|
@ -0,0 +1,524 @@
|
||||||
|
package enchantrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockRandReader is a reader that returns an error.
|
||||||
|
type mockRandReader struct{}
|
||||||
|
|
||||||
|
func (r *mockRandReader) Read(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("random read error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// deterministicReader returns a predictable sequence for testing.
|
||||||
|
type deterministicReader struct {
|
||||||
|
seed byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *deterministicReader) Read(p []byte) (n int, err error) {
|
||||||
|
for i := range p {
|
||||||
|
p[i] = r.seed
|
||||||
|
r.seed++
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ChaChaPolySigil Tests ---
|
||||||
|
|
||||||
|
func TestChaChaPolySigil_Good(t *testing.T) {
|
||||||
|
t.Run("EncryptDecrypt", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("Hello, this is a secret message!")
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, plaintext, ciphertext)
|
||||||
|
assert.Greater(t, len(ciphertext), len(plaintext)) // nonce + overhead
|
||||||
|
|
||||||
|
decrypted, err := sigil.Out(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("EmptyPlaintext", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext, err := sigil.In([]byte{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := sigil.Out(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte{}, decrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LargeData", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with 1MB of data
|
||||||
|
plaintext := make([]byte, 1024*1024)
|
||||||
|
_, err = rand.Read(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := sigil.Out(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DifferentNoncesEachEncryption", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("same message")
|
||||||
|
|
||||||
|
ciphertext1, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext2, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Ciphertexts should differ due to different nonces
|
||||||
|
assert.NotEqual(t, ciphertext1, ciphertext2)
|
||||||
|
|
||||||
|
// But both should decrypt to the same plaintext
|
||||||
|
decrypted1, err := sigil.Out(ciphertext1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
decrypted2, err := sigil.Out(ciphertext2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, plaintext, decrypted1)
|
||||||
|
assert.Equal(t, plaintext, decrypted2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PreObfuscationApplied", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
|
||||||
|
// Use deterministic reader so we can verify obfuscation
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sigil.randReader = &deterministicReader{seed: 0}
|
||||||
|
|
||||||
|
plaintext := []byte("test data")
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The nonce is the first 24 bytes
|
||||||
|
nonce := ciphertext[:24]
|
||||||
|
|
||||||
|
// Verify that pre-obfuscation was applied by checking that
|
||||||
|
// the plaintext pattern doesn't appear in raw form
|
||||||
|
// (The obfuscated data is XORed with a stream derived from the nonce)
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
obfuscated := obfuscator.Obfuscate(plaintext, nonce)
|
||||||
|
assert.NotEqual(t, plaintext, obfuscated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChaChaPolySigil_Bad(t *testing.T) {
|
||||||
|
t.Run("InvalidKeySize", func(t *testing.T) {
|
||||||
|
_, err := NewChaChaPolySigil([]byte("too short"))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
||||||
|
|
||||||
|
_, err = NewChaChaPolySigil(make([]byte, 16))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
||||||
|
|
||||||
|
_, err = NewChaChaPolySigil(make([]byte, 64))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WrongKey", func(t *testing.T) {
|
||||||
|
key1 := make([]byte, 32)
|
||||||
|
key2 := make([]byte, 32)
|
||||||
|
key2[0] = 1 // Different key
|
||||||
|
|
||||||
|
sigil1, err := NewChaChaPolySigil(key1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sigil2, err := NewChaChaPolySigil(key2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext, err := sigil1.In([]byte("secret"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = sigil2.Out(ciphertext)
|
||||||
|
assert.ErrorIs(t, err, ErrDecryptionFailed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TamperedCiphertext", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext, err := sigil.In([]byte("secret"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Tamper with the ciphertext (after the nonce)
|
||||||
|
ciphertext[30] ^= 0xff
|
||||||
|
|
||||||
|
_, err = sigil.Out(ciphertext)
|
||||||
|
assert.ErrorIs(t, err, ErrDecryptionFailed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TruncatedCiphertext", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = sigil.Out([]byte("too short"))
|
||||||
|
assert.ErrorIs(t, err, ErrCiphertextTooShort)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NoKeyConfigured", func(t *testing.T) {
|
||||||
|
sigil := &ChaChaPolySigil{}
|
||||||
|
|
||||||
|
_, err := sigil.In([]byte("test"))
|
||||||
|
assert.ErrorIs(t, err, ErrNoKeyConfigured)
|
||||||
|
|
||||||
|
_, err = sigil.Out([]byte("test"))
|
||||||
|
assert.ErrorIs(t, err, ErrNoKeyConfigured)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RandomReaderError", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sigil.randReader = &mockRandReader{}
|
||||||
|
|
||||||
|
_, err = sigil.In([]byte("test"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChaChaPolySigil_Ugly(t *testing.T) {
|
||||||
|
t.Run("NilPlaintext", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext, err := sigil.In(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, ciphertext)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NilCiphertext", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext, err := sigil.Out(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, plaintext)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NilObfuscator", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sigil.Obfuscator = nil // Explicitly set to nil
|
||||||
|
|
||||||
|
plaintext := []byte("test without obfuscation")
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := sigil.Out(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- XORObfuscator Tests ---
|
||||||
|
|
||||||
|
func TestXORObfuscator_Good(t *testing.T) {
|
||||||
|
t.Run("RoundTrip", func(t *testing.T) {
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
entropy := []byte("random-entropy-value")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
assert.NotEqual(t, data, obfuscated)
|
||||||
|
|
||||||
|
deobfuscated := obfuscator.Deobfuscate(obfuscated, entropy)
|
||||||
|
assert.Equal(t, data, deobfuscated)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DifferentEntropyDifferentOutput", func(t *testing.T) {
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
data := []byte("same data")
|
||||||
|
entropy1 := []byte("entropy1")
|
||||||
|
entropy2 := []byte("entropy2")
|
||||||
|
|
||||||
|
obfuscated1 := obfuscator.Obfuscate(data, entropy1)
|
||||||
|
obfuscated2 := obfuscator.Obfuscate(data, entropy2)
|
||||||
|
|
||||||
|
assert.NotEqual(t, obfuscated1, obfuscated2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LargeData", func(t *testing.T) {
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
data := make([]byte, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte(i % 256)
|
||||||
|
}
|
||||||
|
entropy := []byte("test-entropy")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
deobfuscated := obfuscator.Deobfuscate(obfuscated, entropy)
|
||||||
|
assert.Equal(t, data, deobfuscated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXORObfuscator_Ugly(t *testing.T) {
|
||||||
|
t.Run("EmptyData", func(t *testing.T) {
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
data := []byte{}
|
||||||
|
entropy := []byte("entropy")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
assert.Equal(t, data, obfuscated)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("EmptyEntropy", func(t *testing.T) {
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
data := []byte("test")
|
||||||
|
entropy := []byte{}
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
deobfuscated := obfuscator.Deobfuscate(obfuscated, entropy)
|
||||||
|
assert.Equal(t, data, deobfuscated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ShuffleMaskObfuscator Tests ---
|
||||||
|
|
||||||
|
func TestShuffleMaskObfuscator_Good(t *testing.T) {
|
||||||
|
t.Run("RoundTrip", func(t *testing.T) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
entropy := []byte("random-entropy-value")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
assert.NotEqual(t, data, obfuscated)
|
||||||
|
|
||||||
|
deobfuscated := obfuscator.Deobfuscate(obfuscated, entropy)
|
||||||
|
assert.Equal(t, data, deobfuscated)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DifferentEntropyDifferentOutput", func(t *testing.T) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := []byte("same data")
|
||||||
|
entropy1 := []byte("entropy1")
|
||||||
|
entropy2 := []byte("entropy2")
|
||||||
|
|
||||||
|
obfuscated1 := obfuscator.Obfuscate(data, entropy1)
|
||||||
|
obfuscated2 := obfuscator.Obfuscate(data, entropy2)
|
||||||
|
|
||||||
|
assert.NotEqual(t, obfuscated1, obfuscated2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deterministic", func(t *testing.T) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := []byte("test data")
|
||||||
|
entropy := []byte("same entropy")
|
||||||
|
|
||||||
|
obfuscated1 := obfuscator.Obfuscate(data, entropy)
|
||||||
|
obfuscated2 := obfuscator.Obfuscate(data, entropy)
|
||||||
|
|
||||||
|
assert.Equal(t, obfuscated1, obfuscated2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LargeData", func(t *testing.T) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := make([]byte, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte(i % 256)
|
||||||
|
}
|
||||||
|
entropy := []byte("test-entropy")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
deobfuscated := obfuscator.Deobfuscate(obfuscated, entropy)
|
||||||
|
assert.Equal(t, data, deobfuscated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffleMaskObfuscator_Ugly(t *testing.T) {
|
||||||
|
t.Run("EmptyData", func(t *testing.T) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := []byte{}
|
||||||
|
entropy := []byte("entropy")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
assert.Equal(t, data, obfuscated)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SingleByte", func(t *testing.T) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := []byte{0x42}
|
||||||
|
entropy := []byte("entropy")
|
||||||
|
|
||||||
|
obfuscated := obfuscator.Obfuscate(data, entropy)
|
||||||
|
deobfuscated := obfuscator.Deobfuscate(obfuscated, entropy)
|
||||||
|
assert.Equal(t, data, deobfuscated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GetNonceFromCiphertext Tests ---
|
||||||
|
|
||||||
|
func TestGetNonceFromCiphertext_Good(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ciphertext, err := sigil.In([]byte("test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nonce, err := GetNonceFromCiphertext(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, nonce, 24)
|
||||||
|
|
||||||
|
// Verify the nonce matches the first 24 bytes
|
||||||
|
assert.Equal(t, ciphertext[:24], nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNonceFromCiphertext_Bad(t *testing.T) {
|
||||||
|
_, err := GetNonceFromCiphertext([]byte("too short"))
|
||||||
|
assert.ErrorIs(t, err, ErrCiphertextTooShort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Obfuscator Tests ---
|
||||||
|
|
||||||
|
func TestCustomObfuscator(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
|
||||||
|
t.Run("WithShuffleMaskObfuscator", func(t *testing.T) {
|
||||||
|
sigil, err := NewChaChaPolySigilWithObfuscator(key, &ShuffleMaskObfuscator{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("test with shuffle mask obfuscator")
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := sigil.Out(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithNilObfuscator", func(t *testing.T) {
|
||||||
|
sigil, err := NewChaChaPolySigilWithObfuscator(key, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Default XORObfuscator should be used
|
||||||
|
assert.IsType(t, &XORObfuscator{}, sigil.Obfuscator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Integration Tests ---
|
||||||
|
|
||||||
|
func TestChaChaPolySigil_Integration(t *testing.T) {
|
||||||
|
t.Run("PlaintextNeverInOutput", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use a distinctive pattern that would be easy to find
|
||||||
|
plaintext := []byte("DISTINCTIVE_SECRET_PATTERN_12345")
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The plaintext pattern should not appear anywhere in the ciphertext
|
||||||
|
assert.False(t, bytes.Contains(ciphertext, plaintext))
|
||||||
|
|
||||||
|
// Even substrings should not appear
|
||||||
|
assert.False(t, bytes.Contains(ciphertext, []byte("DISTINCTIVE")))
|
||||||
|
assert.False(t, bytes.Contains(ciphertext, []byte("SECRET")))
|
||||||
|
assert.False(t, bytes.Contains(ciphertext, []byte("PATTERN")))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ConsistentRoundTrip", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i * 7)
|
||||||
|
}
|
||||||
|
sigil, err := NewChaChaPolySigil(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test multiple round trips
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
plaintext := make([]byte, i+1)
|
||||||
|
for j := range plaintext {
|
||||||
|
plaintext[j] = byte(j * i)
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := sigil.Out(ciphertext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted, "Round trip failed for size %d", i+1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Benchmark Tests ---
|
||||||
|
|
||||||
|
func BenchmarkChaChaPolySigil_Encrypt(b *testing.B) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, _ := NewChaChaPolySigil(key)
|
||||||
|
plaintext := make([]byte, 1024)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = sigil.In(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkChaChaPolySigil_Decrypt(b *testing.B) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
sigil, _ := NewChaChaPolySigil(key)
|
||||||
|
plaintext := make([]byte, 1024)
|
||||||
|
ciphertext, _ := sigil.In(plaintext)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = sigil.Out(ciphertext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkXORObfuscator(b *testing.B) {
|
||||||
|
obfuscator := &XORObfuscator{}
|
||||||
|
data := make([]byte, 1024)
|
||||||
|
entropy := make([]byte, 24)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = obfuscator.Obfuscate(data, entropy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkShuffleMaskObfuscator(b *testing.B) {
|
||||||
|
obfuscator := &ShuffleMaskObfuscator{}
|
||||||
|
data := make([]byte, 1024)
|
||||||
|
entropy := make([]byte, 24)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = obfuscator.Obfuscate(data, entropy)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
pkg/enchantrix/enchantrix.go
Normal file
60
pkg/enchantrix/enchantrix.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// 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 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 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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enchantrix defines the interface for acceptance testing.
|
||||||
|
type Enchantrix interface {
|
||||||
|
Transmute(data []byte, sigils []Sigil) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
data, err = sigil.In(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
97
pkg/enchantrix/enchantrix_test.go
Normal file
97
pkg/enchantrix/enchantrix_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
package enchantrix_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Transmute Tests ---
|
||||||
|
|
||||||
|
func TestTransmute_Good(t *testing.T) {
|
||||||
|
data := []byte("hello")
|
||||||
|
sigils := []enchantrix.Sigil{
|
||||||
|
&enchantrix.ReverseSigil{},
|
||||||
|
&enchantrix.HexSigil{},
|
||||||
|
}
|
||||||
|
result, err := enchantrix.Transmute(data, sigils)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "6f6c6c6568", string(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorSigil struct{}
|
||||||
|
|
||||||
|
func (s *errorSigil) In(data []byte) ([]byte, error) {
|
||||||
|
return nil, errors.New("sigil error")
|
||||||
|
}
|
||||||
|
func (s *errorSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
return nil, errors.New("sigil error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransmute_Bad(t *testing.T) {
|
||||||
|
data := []byte("hello")
|
||||||
|
sigils := []enchantrix.Sigil{
|
||||||
|
&enchantrix.ReverseSigil{},
|
||||||
|
&errorSigil{},
|
||||||
|
}
|
||||||
|
_, err := enchantrix.Transmute(data, sigils)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransmute_Ugly(t *testing.T) {
|
||||||
|
// Test with nil data
|
||||||
|
_, err := enchantrix.Transmute(nil, []enchantrix.Sigil{&enchantrix.ReverseSigil{}})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with nil sigils
|
||||||
|
_, err = enchantrix.Transmute([]byte("hello"), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with no sigils
|
||||||
|
result, err := enchantrix.Transmute([]byte("hello"), []enchantrix.Sigil{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", string(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Factory Tests ---
|
||||||
|
|
||||||
|
func TestNewSigil_Good(t *testing.T) {
|
||||||
|
validNames := []string{
|
||||||
|
"reverse", "hex", "base64", "gzip", "json", "json-indent",
|
||||||
|
"md4", "md5", "sha1", "sha224", "sha256", "sha384", "sha512",
|
||||||
|
"ripemd160", "sha3-224", "sha3-256", "sha3-384", "sha3-512",
|
||||||
|
"sha512-224", "sha512-256", "blake2s-256", "blake2b-256",
|
||||||
|
"blake2b-384", "blake2b-512",
|
||||||
|
}
|
||||||
|
for _, name := range validNames {
|
||||||
|
sigil, err := enchantrix.NewSigil(name)
|
||||||
|
assert.NoError(t, err, "Failed to create sigil: %s", name)
|
||||||
|
assert.NotNil(t, sigil, "Sigil should not be nil for name: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSigil_Bad(t *testing.T) {
|
||||||
|
sigil, err := enchantrix.NewSigil("invalid-sigil-name")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, sigil)
|
||||||
|
assert.Contains(t, err.Error(), "unknown sigil name")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSigil_Ugly(t *testing.T) {
|
||||||
|
// Test with empty string
|
||||||
|
sigil, err := enchantrix.NewSigil("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, sigil)
|
||||||
|
|
||||||
|
// Test with whitespace
|
||||||
|
sigil, err = enchantrix.NewSigil(" ")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, sigil)
|
||||||
|
|
||||||
|
// Test with non-printable characters
|
||||||
|
sigil, err = enchantrix.NewSigil("\x00\x01\x02")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, sigil)
|
||||||
|
}
|
||||||
44
pkg/enchantrix/examples_test.go
Normal file
44
pkg/enchantrix/examples_test.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package enchantrix_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleTransmute() {
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
sigils := []enchantrix.Sigil{
|
||||||
|
&enchantrix.ReverseSigil{},
|
||||||
|
&enchantrix.HexSigil{},
|
||||||
|
}
|
||||||
|
transformed, err := enchantrix.Transmute(data, sigils)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Transmute failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Transformed data: %s\n", transformed)
|
||||||
|
// Output:
|
||||||
|
// Transformed data: 21646c726f57202c6f6c6c6548
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewSigil() {
|
||||||
|
sigil, err := enchantrix.NewSigil("base64")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create sigil: %v", err)
|
||||||
|
}
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
encoded, err := sigil.In(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Sigil In failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Encoded data: %s\n", encoded)
|
||||||
|
decoded, err := sigil.Out(encoded)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Sigil Out failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decoded data: %s\n", decoded)
|
||||||
|
// Output:
|
||||||
|
// Encoded data: SGVsbG8sIFdvcmxkIQ==
|
||||||
|
// Decoded data: Hello, World!
|
||||||
|
}
|
||||||
274
pkg/enchantrix/sigils.go
Normal file
274
pkg/enchantrix/sigils.go
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
package enchantrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
"golang.org/x/crypto/blake2s"
|
||||||
|
"golang.org/x/crypto/md4"
|
||||||
|
"golang.org/x/crypto/ripemd160"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReverseSigil is a Sigil that reverses the bytes of the payload.
|
||||||
|
// It is a symmetrical Sigil, meaning that the In and Out methods perform the same operation.
|
||||||
|
type ReverseSigil struct{}
|
||||||
|
|
||||||
|
// In reverses the bytes of the data.
|
||||||
|
func (s *ReverseSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
reversed := make([]byte, len(data))
|
||||||
|
for i, j := 0, len(data)-1; i < len(data); i, j = i+1, j-1 {
|
||||||
|
reversed[i] = data[j]
|
||||||
|
}
|
||||||
|
return reversed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out reverses the bytes of the data.
|
||||||
|
func (s *ReverseSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
return s.In(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HexSigil is a Sigil that encodes/decodes data to/from hexadecimal.
|
||||||
|
// The In method encodes the data, and the Out method decodes it.
|
||||||
|
type HexSigil struct{}
|
||||||
|
|
||||||
|
// In encodes the data to hexadecimal.
|
||||||
|
func (s *HexSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, hex.EncodedLen(len(data)))
|
||||||
|
hex.Encode(dst, data)
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decodes the data from hexadecimal.
|
||||||
|
func (s *HexSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, hex.DecodedLen(len(data)))
|
||||||
|
_, err := hex.Decode(dst, data)
|
||||||
|
return dst, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64Sigil is a Sigil that encodes/decodes data to/from base64.
|
||||||
|
// The In method encodes the data, and the Out method decodes it.
|
||||||
|
type Base64Sigil struct{}
|
||||||
|
|
||||||
|
// In encodes the data to base64.
|
||||||
|
func (s *Base64Sigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.StdEncoding.Encode(dst, data)
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decodes the data from base64.
|
||||||
|
func (s *Base64Sigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.StdEncoding.Decode(dst, data)
|
||||||
|
return dst[:n], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GzipSigil is a Sigil that compresses/decompresses data using gzip.
|
||||||
|
// The In method compresses the data, and the Out method decompresses it.
|
||||||
|
type GzipSigil struct {
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// In compresses the data using gzip.
|
||||||
|
func (s *GzipSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := s.writer
|
||||||
|
if w == nil {
|
||||||
|
w = &b
|
||||||
|
}
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
if _, err := gz.Write(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := gz.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decompresses the data using gzip.
|
||||||
|
func (s *GzipSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
r, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return io.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONSigil is a Sigil that compacts or indents JSON data.
|
||||||
|
// The Out method is a no-op.
|
||||||
|
type JSONSigil struct{ Indent bool }
|
||||||
|
|
||||||
|
// In compacts or indents the JSON data.
|
||||||
|
func (s *JSONSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if s.Indent {
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := json.Indent(&out, data, "", " ")
|
||||||
|
return out.Bytes(), err
|
||||||
|
}
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := json.Compact(&out, data)
|
||||||
|
return out.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out is a no-op for JSONSigil.
|
||||||
|
func (s *JSONSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
// For simplicity, Out is a no-op. The primary use is formatting.
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashSigil is a Sigil that hashes the data using a specified algorithm.
|
||||||
|
// The In method hashes the data, and the Out method is a no-op.
|
||||||
|
type HashSigil struct {
|
||||||
|
Hash crypto.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHashSigil creates a new HashSigil.
|
||||||
|
func NewHashSigil(h crypto.Hash) *HashSigil {
|
||||||
|
return &HashSigil{Hash: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In hashes the data.
|
||||||
|
func (s *HashSigil) In(data []byte) ([]byte, error) {
|
||||||
|
var h io.Writer
|
||||||
|
switch s.Hash {
|
||||||
|
case crypto.MD4:
|
||||||
|
h = md4.New()
|
||||||
|
case crypto.MD5:
|
||||||
|
h = md5.New()
|
||||||
|
case crypto.SHA1:
|
||||||
|
h = sha1.New()
|
||||||
|
case crypto.SHA224:
|
||||||
|
h = sha256.New224()
|
||||||
|
case crypto.SHA256:
|
||||||
|
h = sha256.New()
|
||||||
|
case crypto.SHA384:
|
||||||
|
h = sha512.New384()
|
||||||
|
case crypto.SHA512:
|
||||||
|
h = sha512.New()
|
||||||
|
case crypto.RIPEMD160:
|
||||||
|
h = ripemd160.New()
|
||||||
|
case crypto.SHA3_224:
|
||||||
|
h = sha3.New224()
|
||||||
|
case crypto.SHA3_256:
|
||||||
|
h = sha3.New256()
|
||||||
|
case crypto.SHA3_384:
|
||||||
|
h = sha3.New384()
|
||||||
|
case crypto.SHA3_512:
|
||||||
|
h = sha3.New512()
|
||||||
|
case crypto.SHA512_224:
|
||||||
|
h = sha512.New512_224()
|
||||||
|
case crypto.SHA512_256:
|
||||||
|
h = sha512.New512_256()
|
||||||
|
case crypto.BLAKE2s_256:
|
||||||
|
h, _ = blake2s.New256(nil)
|
||||||
|
case crypto.BLAKE2b_256:
|
||||||
|
h, _ = blake2b.New256(nil)
|
||||||
|
case crypto.BLAKE2b_384:
|
||||||
|
h, _ = blake2b.New384(nil)
|
||||||
|
case crypto.BLAKE2b_512:
|
||||||
|
h, _ = blake2b.New512(nil)
|
||||||
|
default:
|
||||||
|
// MD5SHA1 is not supported as a direct hash
|
||||||
|
return nil, errors.New("enchantrix: hash algorithm not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Write(data)
|
||||||
|
return h.(interface{ Sum([]byte) []byte }).Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out is a no-op for HashSigil.
|
||||||
|
func (s *HashSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigil is a factory function that returns a Sigil based on a string name.
|
||||||
|
// It is the primary way to create Sigil instances.
|
||||||
|
func NewSigil(name string) (Sigil, error) {
|
||||||
|
switch name {
|
||||||
|
case "reverse":
|
||||||
|
return &ReverseSigil{}, nil
|
||||||
|
case "hex":
|
||||||
|
return &HexSigil{}, nil
|
||||||
|
case "base64":
|
||||||
|
return &Base64Sigil{}, nil
|
||||||
|
case "gzip":
|
||||||
|
return &GzipSigil{}, nil
|
||||||
|
case "json":
|
||||||
|
return &JSONSigil{Indent: false}, nil
|
||||||
|
case "json-indent":
|
||||||
|
return &JSONSigil{Indent: true}, nil
|
||||||
|
case "md4":
|
||||||
|
return NewHashSigil(crypto.MD4), nil
|
||||||
|
case "md5":
|
||||||
|
return NewHashSigil(crypto.MD5), nil
|
||||||
|
case "sha1":
|
||||||
|
return NewHashSigil(crypto.SHA1), nil
|
||||||
|
case "sha224":
|
||||||
|
return NewHashSigil(crypto.SHA224), nil
|
||||||
|
case "sha256":
|
||||||
|
return NewHashSigil(crypto.SHA256), nil
|
||||||
|
case "sha384":
|
||||||
|
return NewHashSigil(crypto.SHA384), nil
|
||||||
|
case "sha512":
|
||||||
|
return NewHashSigil(crypto.SHA512), nil
|
||||||
|
case "ripemd160":
|
||||||
|
return NewHashSigil(crypto.RIPEMD160), nil
|
||||||
|
case "sha3-224":
|
||||||
|
return NewHashSigil(crypto.SHA3_224), nil
|
||||||
|
case "sha3-256":
|
||||||
|
return NewHashSigil(crypto.SHA3_256), nil
|
||||||
|
case "sha3-384":
|
||||||
|
return NewHashSigil(crypto.SHA3_384), nil
|
||||||
|
case "sha3-512":
|
||||||
|
return NewHashSigil(crypto.SHA3_512), nil
|
||||||
|
case "sha512-224":
|
||||||
|
return NewHashSigil(crypto.SHA512_224), nil
|
||||||
|
case "sha512-256":
|
||||||
|
return NewHashSigil(crypto.SHA512_256), nil
|
||||||
|
case "blake2s-256":
|
||||||
|
return NewHashSigil(crypto.BLAKE2s_256), nil
|
||||||
|
case "blake2b-256":
|
||||||
|
return NewHashSigil(crypto.BLAKE2b_256), nil
|
||||||
|
case "blake2b-384":
|
||||||
|
return NewHashSigil(crypto.BLAKE2b_384), nil
|
||||||
|
case "blake2b-512":
|
||||||
|
return NewHashSigil(crypto.BLAKE2b_512), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("enchantrix: unknown sigil name")
|
||||||
|
}
|
||||||
|
}
|
||||||
268
pkg/enchantrix/sigils_test.go
Normal file
268
pkg/enchantrix/sigils_test.go
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
package enchantrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockWriter is a writer that fails on Write
|
||||||
|
type mockWriter struct{}
|
||||||
|
|
||||||
|
func (m *mockWriter) Write(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("write error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failOnSecondWrite is a writer that fails on the second write call.
|
||||||
|
type failOnSecondWrite struct {
|
||||||
|
callCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *failOnSecondWrite) Write(p []byte) (n int, err error) {
|
||||||
|
m.callCount++
|
||||||
|
if m.callCount > 1 {
|
||||||
|
return 0, errors.New("second write failed")
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverseSigil_Good(t *testing.T) {
|
||||||
|
s := &ReverseSigil{}
|
||||||
|
data := []byte("hello")
|
||||||
|
reversed, err := s.In(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "olleh", string(reversed))
|
||||||
|
original, err := s.Out(reversed)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", string(original))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverseSigil_Ugly(t *testing.T) {
|
||||||
|
s := &ReverseSigil{}
|
||||||
|
// Test with empty string
|
||||||
|
empty := []byte("")
|
||||||
|
reversedEmpty, err := s.In(empty)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "", string(reversedEmpty))
|
||||||
|
|
||||||
|
// Test with nil
|
||||||
|
reversedNil, err := s.In(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, reversedNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHexSigil_Good(t *testing.T) {
|
||||||
|
s := &HexSigil{}
|
||||||
|
data := []byte("hello")
|
||||||
|
encoded, err := s.In(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "68656c6c6f", string(encoded))
|
||||||
|
decoded, err := s.Out(encoded)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", string(decoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHexSigil_Bad(t *testing.T) {
|
||||||
|
s := &HexSigil{}
|
||||||
|
_, err := s.Out([]byte("not hex"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHexSigil_Ugly(t *testing.T) {
|
||||||
|
s := &HexSigil{}
|
||||||
|
// Test with empty string
|
||||||
|
empty := []byte("")
|
||||||
|
encodedEmpty, err := s.In(empty)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "", string(encodedEmpty))
|
||||||
|
|
||||||
|
// Test with nil
|
||||||
|
encodedNil, err := s.In(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, encodedNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBase64Sigil_Good(t *testing.T) {
|
||||||
|
s := &Base64Sigil{}
|
||||||
|
data := []byte("hello")
|
||||||
|
encoded, err := s.In(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "aGVsbG8=", string(encoded))
|
||||||
|
decoded, err := s.Out(encoded)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", string(decoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBase64Sigil_Bad(t *testing.T) {
|
||||||
|
s := &Base64Sigil{}
|
||||||
|
_, err := s.Out([]byte("not base64"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBase64Sigil_Ugly(t *testing.T) {
|
||||||
|
s := &Base64Sigil{}
|
||||||
|
// Test with empty string
|
||||||
|
empty := []byte("")
|
||||||
|
encodedEmpty, err := s.In(empty)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "", string(encodedEmpty))
|
||||||
|
|
||||||
|
// Test with nil
|
||||||
|
encodedNil, err := s.In(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, encodedNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGzipSigil_Good(t *testing.T) {
|
||||||
|
s := &GzipSigil{}
|
||||||
|
data := []byte("hello")
|
||||||
|
compressed, err := s.In(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, data, compressed)
|
||||||
|
decompressed, err := s.Out(compressed)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", string(decompressed))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGzipSigil_Bad(t *testing.T) {
|
||||||
|
s := &GzipSigil{}
|
||||||
|
data := []byte("hello")
|
||||||
|
|
||||||
|
// Test with invalid gzip data
|
||||||
|
_, err := s.Out([]byte("not gzip"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test writer error
|
||||||
|
s.writer = &mockWriter{}
|
||||||
|
_, err = s.In(data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test closer error
|
||||||
|
s.writer = &failOnSecondWrite{}
|
||||||
|
_, err = s.In(data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGzipSigil_Ugly(t *testing.T) {
|
||||||
|
s := &GzipSigil{}
|
||||||
|
// Test with empty string
|
||||||
|
empty := []byte("")
|
||||||
|
compressedEmpty, err := s.In(empty)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
decompressedEmpty, err := s.Out(compressedEmpty)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "", string(decompressedEmpty))
|
||||||
|
|
||||||
|
// Test with nil
|
||||||
|
compressedNil, err := s.In(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
decompressedNil, err := s.Out(compressedNil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, decompressedNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSigil_Good(t *testing.T) {
|
||||||
|
s := &JSONSigil{Indent: true}
|
||||||
|
data := []byte(`{"hello":"world"}`)
|
||||||
|
indented, err := s.In(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "{\n \"hello\": \"world\"\n}", string(indented))
|
||||||
|
|
||||||
|
s.Indent = false
|
||||||
|
compacted, err := s.In(indented)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"hello":"world"}`, string(compacted))
|
||||||
|
|
||||||
|
// Out is a no-op
|
||||||
|
outData, err := s.Out(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, data, outData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSigil_Bad(t *testing.T) {
|
||||||
|
s := &JSONSigil{}
|
||||||
|
_, err := s.In([]byte("not json"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSigil_Ugly(t *testing.T) {
|
||||||
|
s := &JSONSigil{}
|
||||||
|
// Test with empty string
|
||||||
|
empty := []byte("")
|
||||||
|
_, err := s.In(empty)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test with nil
|
||||||
|
_, err = s.In(nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashSigils_Good(t *testing.T) {
|
||||||
|
// Using the input "hello" for all hash tests
|
||||||
|
data := []byte("hello")
|
||||||
|
|
||||||
|
// A map of hash names to their expected hex-encoded output for the input "hello"
|
||||||
|
expectedHashes := map[string]string{
|
||||||
|
"md4": "866437cb7a794bce2b727acc0362ee27",
|
||||||
|
"md5": "5d41402abc4b2a76b9719d911017c592",
|
||||||
|
"sha1": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
|
||||||
|
"sha224": "ea09ae9cc6768c50fcee903ed054556e5bfc8347907f12598aa24193",
|
||||||
|
"sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
||||||
|
"sha384": "59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f",
|
||||||
|
"sha512": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043",
|
||||||
|
"ripemd160": "108f07b8382412612c048d07d13f814118445acd",
|
||||||
|
"sha3-224": "b87f88c72702fff1748e58b87e9141a42c0dbedc29a78cb0d4a5cd81",
|
||||||
|
"sha3-256": "3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392",
|
||||||
|
"sha3-384": "720aea11019ef06440fbf05d87aa24680a2153df3907b23631e7177ce620fa1330ff07c0fddee54699a4c3ee0ee9d887",
|
||||||
|
"sha3-512": "75d527c368f2efe848ecf6b073a36767800805e9eef2b1857d5f984f036eb6df891d75f72d9b154518c1cd58835286d1da9a38deba3de98b5a53e5ed78a84976",
|
||||||
|
"sha512-224": "fe8509ed1fb7dcefc27e6ac1a80eddbec4cb3d2c6fe565244374061c",
|
||||||
|
"sha512-256": "e30d87cfa2a75db545eac4d61baf970366a8357c7f72fa95b52d0accb698f13a",
|
||||||
|
"blake2s-256": "19213bacc58dee6dbde3ceb9a47cbb330b3d86f8cca8997eb00be456f140ca25",
|
||||||
|
"blake2b-256": "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf",
|
||||||
|
"blake2b-384": "85f19170be541e7774da197c12ce959b91a280b2f23e3113d6638a3335507ed72ddc30f81244dbe9fa8d195c23bceb7e",
|
||||||
|
"blake2b-512": "e4cfa39a3d37be31c59609e807970799caa68a19bfaa15135f165085e01d41a65ba1e1b146aeb6bd0092b49eac214c103ccfa3a365954bbbe52f74a2b3620c94",
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, expectedHex := range expectedHashes {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
s, err := NewSigil(name)
|
||||||
|
assert.NoError(t, err, "Failed to create sigil: %s", name)
|
||||||
|
|
||||||
|
hashed, err := s.In(data)
|
||||||
|
assert.NoError(t, err, "Hashing failed for sigil: %s", name)
|
||||||
|
assert.Equal(t, expectedHex, hex.EncodeToString(hashed), "Hash mismatch for sigil: %s", name)
|
||||||
|
|
||||||
|
// Also test the Out function, which should be a no-op
|
||||||
|
unhashed, err := s.Out(hashed)
|
||||||
|
assert.NoError(t, err, "Out failed for sigil: %s", name)
|
||||||
|
assert.Equal(t, hashed, unhashed, "Out should be a no-op for sigil: %s", name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashSigil_Bad(t *testing.T) {
|
||||||
|
// 99 is not a valid crypto.Hash value
|
||||||
|
s := NewHashSigil(99)
|
||||||
|
data := []byte("hello")
|
||||||
|
_, err := s.In(data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "hash algorithm not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashSigil_Ugly(t *testing.T) {
|
||||||
|
s, err := NewSigil("sha256")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with empty string
|
||||||
|
empty := []byte("")
|
||||||
|
hashedEmpty, err := s.In(empty)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, hashedEmpty)
|
||||||
|
|
||||||
|
// Test with nil
|
||||||
|
hashedNil, err := s.In(nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, hashedNil)
|
||||||
|
}
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package pool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Snider/Enchantrix/pkg/miner"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PoolClient struct {
|
|
||||||
URL string
|
|
||||||
User string
|
|
||||||
Pass string
|
|
||||||
JobQueue *miner.JobQueue
|
|
||||||
stopChannel chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(url, user, pass string, jobQueue *miner.JobQueue) *PoolClient {
|
|
||||||
return &PoolClient{
|
|
||||||
URL: url,
|
|
||||||
User: user,
|
|
||||||
Pass: pass,
|
|
||||||
JobQueue: jobQueue,
|
|
||||||
stopChannel: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PoolClient) Start() {
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
p.JobQueue.Set(miner.NewMockJob())
|
|
||||||
case <-p.stopChannel:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PoolClient) Stop() {
|
|
||||||
close(p.stopChannel)
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package pool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Snider/Enchantrix/pkg/miner"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPoolClient(t *testing.T) {
|
|
||||||
jq := miner.NewJobQueue()
|
|
||||||
pc := New("test-url", "test-user", "test-pass", jq)
|
|
||||||
pc.Start()
|
|
||||||
time.Sleep(6 * time.Second)
|
|
||||||
pc.Stop()
|
|
||||||
|
|
||||||
assert.NotNil(t, jq.Get())
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Proxy struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
StartTime time.Time
|
|
||||||
Workers []*Worker
|
|
||||||
stopChannel chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Worker struct {
|
|
||||||
ID string
|
|
||||||
Hashrate float64
|
|
||||||
Shares int
|
|
||||||
Connected time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Proxy {
|
|
||||||
return &Proxy{
|
|
||||||
StartTime: time.Now(),
|
|
||||||
stopChannel: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) Start() {
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
p.mu.Lock()
|
|
||||||
// Simulate a worker connecting
|
|
||||||
if len(p.Workers) < 10 {
|
|
||||||
p.Workers = append(p.Workers, &Worker{
|
|
||||||
ID: fmt.Sprintf("worker-%d", len(p.Workers)),
|
|
||||||
Hashrate: 100 + rand.Float64()*10-5,
|
|
||||||
Connected: time.Now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
case <-p.stopChannel:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) Stop() {
|
|
||||||
close(p.stopChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) Summary() map[string]interface{} {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
|
|
||||||
var totalHashrate float64
|
|
||||||
for _, worker := range p.Workers {
|
|
||||||
totalHashrate += worker.Hashrate
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]interface{}{
|
|
||||||
"id": "enchantrix-proxy",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"kind": "proxy",
|
|
||||||
"uptime": int64(time.Since(p.StartTime).Seconds()),
|
|
||||||
"hashrate": map[string]interface{}{
|
|
||||||
"total": []float64{totalHashrate, totalHashrate, totalHashrate},
|
|
||||||
},
|
|
||||||
"miners": map[string]interface{}{
|
|
||||||
"now": len(p.Workers),
|
|
||||||
"max": 10,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) WorkersSummary() []map[string]interface{} {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
|
|
||||||
summary := make([]map[string]interface{}, len(p.Workers))
|
|
||||||
for i, worker := range p.Workers {
|
|
||||||
summary[i] = map[string]interface{}{
|
|
||||||
"id": worker.ID,
|
|
||||||
"hashrate": worker.Hashrate,
|
|
||||||
"shares": worker.Shares,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return summary
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestProxy(t *testing.T) {
|
|
||||||
proxy := New()
|
|
||||||
proxy.Start()
|
|
||||||
time.Sleep(6 * time.Second)
|
|
||||||
proxy.Stop()
|
|
||||||
|
|
||||||
summary := proxy.Summary()
|
|
||||||
assert.NotNil(t, summary)
|
|
||||||
|
|
||||||
workers := proxy.WorkersSummary()
|
|
||||||
assert.NotNil(t, workers)
|
|
||||||
assert.True(t, len(workers) > 0)
|
|
||||||
}
|
|
||||||
189
pkg/trix/crypto.go
Normal file
189
pkg/trix/crypto.go
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
package trix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoEncryptionKey is returned when encryption is requested without a key.
|
||||||
|
ErrNoEncryptionKey = errors.New("trix: encryption key not configured")
|
||||||
|
// ErrAlreadyEncrypted is returned when trying to encrypt already encrypted data.
|
||||||
|
ErrAlreadyEncrypted = errors.New("trix: payload is already encrypted")
|
||||||
|
// ErrNotEncrypted is returned when trying to decrypt non-encrypted data.
|
||||||
|
ErrNotEncrypted = errors.New("trix: payload is not encrypted")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HeaderKeyEncrypted indicates whether the payload is encrypted.
|
||||||
|
HeaderKeyEncrypted = "encrypted"
|
||||||
|
// HeaderKeyAlgorithm stores the encryption algorithm used.
|
||||||
|
HeaderKeyAlgorithm = "encryption_algorithm"
|
||||||
|
// HeaderKeyEncryptedAt stores when the payload was encrypted.
|
||||||
|
HeaderKeyEncryptedAt = "encrypted_at"
|
||||||
|
// HeaderKeyObfuscator stores the obfuscator type used.
|
||||||
|
HeaderKeyObfuscator = "obfuscator"
|
||||||
|
|
||||||
|
// AlgorithmChaCha20Poly1305 is the identifier for ChaCha20-Poly1305.
|
||||||
|
AlgorithmChaCha20Poly1305 = "xchacha20-poly1305"
|
||||||
|
// ObfuscatorXOR identifies the XOR obfuscator.
|
||||||
|
ObfuscatorXOR = "xor"
|
||||||
|
// ObfuscatorShuffleMask identifies the shuffle-mask obfuscator.
|
||||||
|
ObfuscatorShuffleMask = "shuffle-mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CryptoConfig holds encryption configuration for a Trix container.
|
||||||
|
type CryptoConfig struct {
|
||||||
|
// Key is the 32-byte encryption key.
|
||||||
|
Key []byte
|
||||||
|
// Obfuscator type: "xor" (default) or "shuffle-mask"
|
||||||
|
Obfuscator string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptPayload encrypts the Trix payload using ChaCha20-Poly1305 with pre-obfuscation.
|
||||||
|
//
|
||||||
|
// The nonce is embedded in the ciphertext itself and is NOT stored separately
|
||||||
|
// in the header. This is the production-ready approach (not demo-style).
|
||||||
|
//
|
||||||
|
// Header metadata is updated to indicate encryption status without exposing
|
||||||
|
// cryptographic parameters that are already embedded in the ciphertext.
|
||||||
|
func (t *Trix) EncryptPayload(config *CryptoConfig) error {
|
||||||
|
if config == nil || len(config.Key) != 32 {
|
||||||
|
return ErrNoEncryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already encrypted
|
||||||
|
if encrypted, ok := t.Header[HeaderKeyEncrypted].(bool); ok && encrypted {
|
||||||
|
return ErrAlreadyEncrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the obfuscator
|
||||||
|
var obfuscator enchantrix.PreObfuscator
|
||||||
|
obfuscatorName := ObfuscatorXOR
|
||||||
|
switch config.Obfuscator {
|
||||||
|
case ObfuscatorShuffleMask:
|
||||||
|
obfuscator = &enchantrix.ShuffleMaskObfuscator{}
|
||||||
|
obfuscatorName = ObfuscatorShuffleMask
|
||||||
|
default:
|
||||||
|
obfuscator = &enchantrix.XORObfuscator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the encryption sigil
|
||||||
|
sigil, err := enchantrix.NewChaChaPolySigilWithObfuscator(config.Key, obfuscator)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the payload
|
||||||
|
ciphertext, err := sigil.In(t.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update payload with ciphertext
|
||||||
|
t.Payload = ciphertext
|
||||||
|
|
||||||
|
// Update header with encryption metadata
|
||||||
|
// NOTE: We do NOT store the nonce in the header - it's embedded in the ciphertext
|
||||||
|
if t.Header == nil {
|
||||||
|
t.Header = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
t.Header[HeaderKeyEncrypted] = true
|
||||||
|
t.Header[HeaderKeyAlgorithm] = AlgorithmChaCha20Poly1305
|
||||||
|
t.Header[HeaderKeyObfuscator] = obfuscatorName
|
||||||
|
t.Header[HeaderKeyEncryptedAt] = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptPayload decrypts the Trix payload using the provided key.
|
||||||
|
//
|
||||||
|
// The nonce is extracted from the ciphertext itself - no need to read it
|
||||||
|
// from the header separately.
|
||||||
|
func (t *Trix) DecryptPayload(config *CryptoConfig) error {
|
||||||
|
if config == nil || len(config.Key) != 32 {
|
||||||
|
return ErrNoEncryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if encrypted
|
||||||
|
encrypted, ok := t.Header[HeaderKeyEncrypted].(bool)
|
||||||
|
if !ok || !encrypted {
|
||||||
|
return ErrNotEncrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine obfuscator from header
|
||||||
|
var obfuscator enchantrix.PreObfuscator
|
||||||
|
if obfType, ok := t.Header[HeaderKeyObfuscator].(string); ok {
|
||||||
|
switch obfType {
|
||||||
|
case ObfuscatorShuffleMask:
|
||||||
|
obfuscator = &enchantrix.ShuffleMaskObfuscator{}
|
||||||
|
default:
|
||||||
|
obfuscator = &enchantrix.XORObfuscator{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obfuscator = &enchantrix.XORObfuscator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the decryption sigil
|
||||||
|
sigil, err := enchantrix.NewChaChaPolySigilWithObfuscator(config.Key, obfuscator)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the payload
|
||||||
|
plaintext, err := sigil.Out(t.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update payload with plaintext
|
||||||
|
t.Payload = plaintext
|
||||||
|
|
||||||
|
// Update header to indicate decrypted state
|
||||||
|
t.Header[HeaderKeyEncrypted] = false
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEncrypted returns true if the payload is currently encrypted.
|
||||||
|
func (t *Trix) IsEncrypted() bool {
|
||||||
|
if t.Header == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
encrypted, ok := t.Header[HeaderKeyEncrypted].(bool)
|
||||||
|
return ok && encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptionAlgorithm returns the encryption algorithm used, if any.
|
||||||
|
func (t *Trix) GetEncryptionAlgorithm() string {
|
||||||
|
if t.Header == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
algo, ok := t.Header[HeaderKeyAlgorithm].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return algo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncryptedTrix creates a new Trix container with an encrypted payload.
|
||||||
|
// This is a convenience function for creating encrypted containers in one step.
|
||||||
|
func NewEncryptedTrix(payload []byte, key []byte, header map[string]interface{}) (*Trix, error) {
|
||||||
|
if header == nil {
|
||||||
|
header = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &Trix{
|
||||||
|
Header: header,
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &CryptoConfig{Key: key}
|
||||||
|
if err := t.EncryptPayload(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
438
pkg/trix/crypto_test.go
Normal file
438
pkg/trix/crypto_test.go
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
package trix_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/trix"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncryptPayload_Good(t *testing.T) {
|
||||||
|
t.Run("BasicEncryption", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPayload := []byte("This is a secret message that should be encrypted.")
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{"content_type": "text/plain"},
|
||||||
|
Payload: originalPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify encryption occurred
|
||||||
|
assert.True(t, trixContainer.IsEncrypted())
|
||||||
|
assert.Equal(t, trix.AlgorithmChaCha20Poly1305, trixContainer.GetEncryptionAlgorithm())
|
||||||
|
assert.NotEqual(t, originalPayload, trixContainer.Payload)
|
||||||
|
|
||||||
|
// Verify header metadata
|
||||||
|
assert.Equal(t, true, trixContainer.Header[trix.HeaderKeyEncrypted])
|
||||||
|
assert.Equal(t, trix.AlgorithmChaCha20Poly1305, trixContainer.Header[trix.HeaderKeyAlgorithm])
|
||||||
|
assert.Equal(t, trix.ObfuscatorXOR, trixContainer.Header[trix.HeaderKeyObfuscator])
|
||||||
|
assert.NotEmpty(t, trixContainer.Header[trix.HeaderKeyEncryptedAt])
|
||||||
|
|
||||||
|
// Verify NO nonce in header (this is the key improvement over demo-style)
|
||||||
|
_, hasNonce := trixContainer.Header["nonce"]
|
||||||
|
assert.False(t, hasNonce, "nonce should NOT be stored in header")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithShuffleMaskObfuscator", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
payload := []byte("test data")
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{
|
||||||
|
Key: key,
|
||||||
|
Obfuscator: trix.ObfuscatorShuffleMask,
|
||||||
|
}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, trix.ObfuscatorShuffleMask, trixContainer.Header[trix.HeaderKeyObfuscator])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithNilHeader", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Payload: []byte("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, trixContainer.Header)
|
||||||
|
assert.True(t, trixContainer.IsEncrypted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptPayload_Bad(t *testing.T) {
|
||||||
|
t.Run("NilConfig", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{Payload: []byte("test")}
|
||||||
|
err := trixContainer.EncryptPayload(nil)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidKeySize", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{Payload: []byte("test")}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: []byte("too short")}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlreadyEncrypted", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
||||||
|
Payload: []byte("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrAlreadyEncrypted)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptPayload_Good(t *testing.T) {
|
||||||
|
t.Run("BasicDecryption", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPayload := []byte("This is a secret message that should be encrypted.")
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: originalPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, trixContainer.IsEncrypted())
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
err = trixContainer.DecryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, trixContainer.IsEncrypted())
|
||||||
|
assert.Equal(t, originalPayload, trixContainer.Payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithShuffleMaskObfuscator", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
originalPayload := []byte("test with shuffle mask")
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: originalPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{
|
||||||
|
Key: key,
|
||||||
|
Obfuscator: trix.ObfuscatorShuffleMask,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = trixContainer.DecryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, originalPayload, trixContainer.Payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("EmptyPayload", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = trixContainer.DecryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte{}, trixContainer.Payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptPayload_Bad(t *testing.T) {
|
||||||
|
t.Run("NilConfig", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
||||||
|
Payload: []byte("encrypted data"),
|
||||||
|
}
|
||||||
|
err := trixContainer.DecryptPayload(nil)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidKeySize", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
||||||
|
Payload: []byte("encrypted data"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: []byte("too short")}
|
||||||
|
err := trixContainer.DecryptPayload(config)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NotEncrypted", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("not encrypted"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.DecryptPayload(config)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrNotEncrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WrongKey", func(t *testing.T) {
|
||||||
|
key1 := make([]byte, 32)
|
||||||
|
key2 := make([]byte, 32)
|
||||||
|
key2[0] = 1
|
||||||
|
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("secret"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config1 := &trix.CryptoConfig{Key: key1}
|
||||||
|
err := trixContainer.EncryptPayload(config1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
config2 := &trix.CryptoConfig{Key: key2}
|
||||||
|
err = trixContainer.DecryptPayload(config2)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptPayload_Ugly(t *testing.T) {
|
||||||
|
t.Run("MissingObfuscatorHeader", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Remove the obfuscator header
|
||||||
|
delete(trixContainer.Header, trix.HeaderKeyObfuscator)
|
||||||
|
|
||||||
|
// Should still work with default XOR obfuscator
|
||||||
|
err = trixContainer.DecryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEncryptedTrix_Good(t *testing.T) {
|
||||||
|
t.Run("Basic", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
payload := []byte("secret message")
|
||||||
|
header := map[string]interface{}{"custom": "value"}
|
||||||
|
|
||||||
|
trixContainer, err := trix.NewEncryptedTrix(payload, key, header)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, trixContainer.IsEncrypted())
|
||||||
|
assert.Equal(t, "value", trixContainer.Header["custom"])
|
||||||
|
assert.NotEqual(t, payload, trixContainer.Payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithNilHeader", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
payload := []byte("secret message")
|
||||||
|
|
||||||
|
trixContainer, err := trix.NewEncryptedTrix(payload, key, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, trixContainer.IsEncrypted())
|
||||||
|
assert.NotNil(t, trixContainer.Header)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEncryptedTrix_Bad(t *testing.T) {
|
||||||
|
t.Run("InvalidKey", func(t *testing.T) {
|
||||||
|
_, err := trix.NewEncryptedTrix([]byte("test"), []byte("short"), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsEncrypted(t *testing.T) {
|
||||||
|
t.Run("NilHeader", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{}
|
||||||
|
assert.False(t, trixContainer.IsEncrypted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MissingKey", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{Header: map[string]interface{}{}}
|
||||||
|
assert.False(t, trixContainer.IsEncrypted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FalseValue", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: false},
|
||||||
|
}
|
||||||
|
assert.False(t, trixContainer.IsEncrypted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TrueValue", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
||||||
|
}
|
||||||
|
assert.True(t, trixContainer.IsEncrypted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WrongType", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: "true"},
|
||||||
|
}
|
||||||
|
assert.False(t, trixContainer.IsEncrypted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEncryptionAlgorithm(t *testing.T) {
|
||||||
|
t.Run("NilHeader", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{}
|
||||||
|
assert.Empty(t, trixContainer.GetEncryptionAlgorithm())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MissingKey", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{Header: map[string]interface{}{}}
|
||||||
|
assert.Empty(t, trixContainer.GetEncryptionAlgorithm())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ValidAlgorithm", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyAlgorithm: "test-algo"},
|
||||||
|
}
|
||||||
|
assert.Equal(t, "test-algo", trixContainer.GetEncryptionAlgorithm())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WrongType", func(t *testing.T) {
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{trix.HeaderKeyAlgorithm: 123},
|
||||||
|
}
|
||||||
|
assert.Empty(t, trixContainer.GetEncryptionAlgorithm())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedTrixRoundTrip(t *testing.T) {
|
||||||
|
t.Run("FullRoundTrip", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i * 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPayload := []byte("This is the original secret message that will be encrypted, stored, and decrypted.")
|
||||||
|
header := map[string]interface{}{
|
||||||
|
"content_type": "text/plain",
|
||||||
|
"custom_field": "custom_value",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create encrypted Trix
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: header,
|
||||||
|
Payload: originalPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Encode to binary format
|
||||||
|
encoded, err := trix.Encode(trixContainer, "ENCR", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Decode from binary format
|
||||||
|
decoded, err := trix.Decode(encoded, "ENCR", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify still encrypted after decode
|
||||||
|
assert.True(t, decoded.IsEncrypted())
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
err = decoded.DecryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify payload matches original
|
||||||
|
assert.Equal(t, originalPayload, decoded.Payload)
|
||||||
|
assert.Equal(t, "custom_value", decoded.Header["custom_field"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonceNotInHeader(t *testing.T) {
|
||||||
|
t.Run("NonceEmbeddedNotExposed", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("secret data"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify nonce is NOT in header
|
||||||
|
_, hasNonce := trixContainer.Header["nonce"]
|
||||||
|
assert.False(t, hasNonce)
|
||||||
|
|
||||||
|
// But the ciphertext contains the nonce (first 24 bytes)
|
||||||
|
assert.GreaterOrEqual(t, len(trixContainer.Payload), 24)
|
||||||
|
|
||||||
|
// Encode and decode
|
||||||
|
encoded, err := trix.Encode(trixContainer, "TEST", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decoded, err := trix.Decode(encoded, "TEST", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Still no nonce in header after decode
|
||||||
|
_, hasNonce = decoded.Header["nonce"]
|
||||||
|
assert.False(t, hasNonce)
|
||||||
|
|
||||||
|
// But decryption still works (nonce is embedded in payload)
|
||||||
|
err = decoded.DecryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("secret data"), decoded.Payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaintextNotExposed(t *testing.T) {
|
||||||
|
t.Run("CleartextNeverInCiphertext", func(t *testing.T) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
distinctivePayload := []byte("DISTINCTIVE_SECRET_PATTERN_THAT_SHOULD_NOT_APPEAR")
|
||||||
|
|
||||||
|
trixContainer := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: distinctivePayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &trix.CryptoConfig{Key: key}
|
||||||
|
err := trixContainer.EncryptPayload(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The plaintext should not appear in the encrypted payload
|
||||||
|
assert.False(t, bytes.Contains(trixContainer.Payload, distinctivePayload))
|
||||||
|
assert.False(t, bytes.Contains(trixContainer.Payload, []byte("DISTINCTIVE")))
|
||||||
|
assert.False(t, bytes.Contains(trixContainer.Payload, []byte("SECRET")))
|
||||||
|
assert.False(t, bytes.Contains(trixContainer.Payload, []byte("PATTERN")))
|
||||||
|
})
|
||||||
|
}
|
||||||
93
pkg/trix/examples_test.go
Normal file
93
pkg/trix/examples_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
package trix_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/trix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleEncode() {
|
||||||
|
t := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{"author": "Jules"},
|
||||||
|
Payload: []byte("Hello, Trix!"),
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(t, "TRIX", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Encode failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Encoded data is not empty: %v\n", len(encoded) > 0)
|
||||||
|
// Output:
|
||||||
|
// Encoded data is not empty: true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDecode() {
|
||||||
|
t := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{"author": "Jules"},
|
||||||
|
Payload: []byte("Hello, Trix!"),
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(t, "TRIX", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Encode failed: %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := trix.Decode(encoded, "TRIX", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Decode failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decoded payload: %s\n", decoded.Payload)
|
||||||
|
fmt.Printf("Decoded header: %v\n", decoded.Header)
|
||||||
|
// Output:
|
||||||
|
// Decoded payload: Hello, Trix!
|
||||||
|
// Decoded header: map[author:Jules]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleTrix_Pack() {
|
||||||
|
t := &trix.Trix{
|
||||||
|
Payload: []byte("secret message"),
|
||||||
|
InSigils: []string{"base64", "reverse"},
|
||||||
|
}
|
||||||
|
err := t.Pack()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Pack failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Packed payload: %s\n", t.Payload)
|
||||||
|
// Output:
|
||||||
|
// Packed payload: =U2ZhN3cl1GI0VmcjV2c
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleTrix_Unpack() {
|
||||||
|
t := &trix.Trix{
|
||||||
|
Payload: []byte("=U2ZhN3cl1GI0VmcjV2c"),
|
||||||
|
OutSigils: []string{"base64", "reverse"},
|
||||||
|
}
|
||||||
|
err := t.Unpack()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unpack failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Unpacked payload: %s\n", t.Payload)
|
||||||
|
// Output:
|
||||||
|
// Unpacked payload: secret message
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleTrix_Pack_checksum() {
|
||||||
|
t := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("secret message"),
|
||||||
|
InSigils: []string{"base64", "reverse"},
|
||||||
|
ChecksumAlgo: crypt.SHA256,
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(t, "TRIX", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Encode failed: %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := trix.Decode(encoded, "TRIX", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Decode failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Decoded payload: %s\n", decoded.Payload)
|
||||||
|
fmt.Printf("Checksum verified: %v\n", decoded.Header["checksum"] != nil)
|
||||||
|
// Output:
|
||||||
|
// Decoded payload: secret message
|
||||||
|
// Checksum verified: true
|
||||||
|
}
|
||||||
2
pkg/trix/testdata/fuzz/FuzzDecode/d02802ef987b399b
vendored
Normal file
2
pkg/trix/testdata/fuzz/FuzzDecode/d02802ef987b399b
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("FUZZ\x02in\"\"")
|
||||||
194
pkg/trix/trix.go
194
pkg/trix/trix.go
|
|
@ -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
|
package trix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -7,86 +27,143 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Version is the current version of the .trix file format.
|
||||||
|
// See RFC-0002 for version history and compatibility notes.
|
||||||
Version = 2
|
Version = 2
|
||||||
|
// 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
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// ErrInvalidMagicNumber is returned when the magic number is incorrect.
|
||||||
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
|
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
|
||||||
ErrInvalidVersion = errors.New("trix: invalid version")
|
// ErrInvalidVersion is returned when the version is incorrect.
|
||||||
ErrMagicNumberLength = errors.New("trix: magic number must be 4 bytes long")
|
ErrInvalidVersion = errors.New("trix: invalid version")
|
||||||
ErrNilSigil = errors.New("trix: sigil cannot be nil")
|
// ErrMagicNumberLength is returned when the magic number is not 4 bytes long.
|
||||||
|
ErrMagicNumberLength = errors.New("trix: magic number must be 4 bytes long")
|
||||||
|
// ErrNilSigil is returned when a sigil is nil.
|
||||||
|
ErrNilSigil = errors.New("trix: sigil cannot be nil")
|
||||||
|
// ErrChecksumMismatch is returned when the checksum does not match.
|
||||||
|
ErrChecksumMismatch = errors.New("trix: checksum mismatch")
|
||||||
|
// ErrHeaderTooLarge is returned when the header size exceeds the maximum allowed.
|
||||||
|
ErrHeaderTooLarge = errors.New("trix: header size exceeds maximum allowed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sigil defines the interface for a data transformer.
|
// Trix represents a .trix container with header metadata and binary payload.
|
||||||
type Sigil interface {
|
//
|
||||||
In(data []byte) ([]byte, error)
|
// The Header field holds arbitrary JSON-serializable metadata. Common fields include:
|
||||||
Out(data []byte) ([]byte, error)
|
// - content_type: MIME type of the original payload
|
||||||
}
|
// - created_at: ISO 8601 timestamp
|
||||||
|
// - encryption_algorithm: Algorithm used for encryption (if applicable)
|
||||||
// Trix represents the structure of a .trix file.
|
// - 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 {
|
type Trix struct {
|
||||||
Header map[string]interface{}
|
// Header contains JSON-serializable metadata about the payload.
|
||||||
|
Header map[string]interface{}
|
||||||
|
// Payload is the binary data stored in the container.
|
||||||
Payload []byte
|
Payload []byte
|
||||||
Sigils []Sigil `json:"-"` // Ignore Sigils during JSON marshaling
|
// 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:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode serializes a Trix struct into the .trix binary format.
|
// Encode serializes a Trix struct into the .trix binary format.
|
||||||
func Encode(trix *Trix, magicNumber string) ([]byte, error) {
|
// It returns the encoded data as a byte slice.
|
||||||
|
func Encode(trix *Trix, magicNumber string, w io.Writer) ([]byte, error) {
|
||||||
if len(magicNumber) != 4 {
|
if len(magicNumber) != 4 {
|
||||||
return nil, ErrMagicNumberLength
|
return nil, ErrMagicNumberLength
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate and add checksum if an algorithm is specified
|
||||||
|
if trix.ChecksumAlgo != "" {
|
||||||
|
checksum := crypt.NewService().Hash(trix.ChecksumAlgo, string(trix.Payload))
|
||||||
|
trix.Header["checksum"] = checksum
|
||||||
|
trix.Header["checksum_algo"] = string(trix.ChecksumAlgo)
|
||||||
|
}
|
||||||
|
|
||||||
headerBytes, err := json.Marshal(trix.Header)
|
headerBytes, err := json.Marshal(trix.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
headerLength := uint32(len(headerBytes))
|
headerLength := uint32(len(headerBytes))
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
// If no writer is provided, use an internal buffer.
|
||||||
|
// This maintains the original function signature's behavior of returning the byte slice.
|
||||||
|
var buf *bytes.Buffer
|
||||||
|
writer := w
|
||||||
|
if writer == nil {
|
||||||
|
buf = new(bytes.Buffer)
|
||||||
|
writer = buf
|
||||||
|
}
|
||||||
|
|
||||||
// Write Magic Number
|
// Write Magic Number
|
||||||
if _, err := buf.WriteString(magicNumber); err != nil {
|
if _, err := io.WriteString(writer, magicNumber); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write Version
|
// Write Version
|
||||||
if err := buf.WriteByte(byte(Version)); err != nil {
|
if _, err := writer.Write([]byte{byte(Version)}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write Header Length
|
// Write Header Length
|
||||||
if err := binary.Write(buf, binary.BigEndian, headerLength); err != nil {
|
if err := binary.Write(writer, binary.BigEndian, headerLength); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write JSON Header
|
// Write JSON Header
|
||||||
if _, err := buf.Write(headerBytes); err != nil {
|
if _, err := writer.Write(headerBytes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write Payload
|
// Write Payload
|
||||||
if _, err := buf.Write(trix.Payload); err != nil {
|
if _, err := writer.Write(trix.Payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
// If we used our internal buffer, return its bytes.
|
||||||
|
if buf != nil {
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an external writer was used, we can't return the bytes.
|
||||||
|
// The caller is responsible for the writer.
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode deserializes the .trix binary format into a Trix struct.
|
// Decode deserializes the .trix binary format into a Trix struct.
|
||||||
|
// It returns the decoded Trix struct.
|
||||||
// Note: Sigils are not stored in the format and must be re-attached by the caller.
|
// Note: Sigils are not stored in the format and must be re-attached by the caller.
|
||||||
func Decode(data []byte, magicNumber string) (*Trix, error) {
|
func Decode(data []byte, magicNumber string, r io.Reader) (*Trix, error) {
|
||||||
if len(magicNumber) != 4 {
|
if len(magicNumber) != 4 {
|
||||||
return nil, ErrMagicNumberLength
|
return nil, ErrMagicNumberLength
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewReader(data)
|
var reader io.Reader
|
||||||
|
if r != nil {
|
||||||
|
reader = r
|
||||||
|
} else {
|
||||||
|
reader = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
// Read and Verify Magic Number
|
// Read and Verify Magic Number
|
||||||
magic := make([]byte, 4)
|
magic := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(buf, magic); err != nil {
|
if _, err := io.ReadFull(reader, magic); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if string(magic) != magicNumber {
|
if string(magic) != magicNumber {
|
||||||
|
|
@ -94,23 +171,28 @@ func Decode(data []byte, magicNumber string) (*Trix, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and Verify Version
|
// Read and Verify Version
|
||||||
version, err := buf.ReadByte()
|
versionByte := make([]byte, 1)
|
||||||
if err != nil {
|
if _, err := io.ReadFull(reader, versionByte); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if version != Version {
|
if versionByte[0] != Version {
|
||||||
return nil, ErrInvalidVersion
|
return nil, ErrInvalidVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read Header Length
|
// Read Header Length
|
||||||
var headerLength uint32
|
var headerLength uint32
|
||||||
if err := binary.Read(buf, binary.BigEndian, &headerLength); err != nil {
|
if err := binary.Read(reader, binary.BigEndian, &headerLength); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanity check the header length to prevent massive allocations.
|
||||||
|
if headerLength > MaxHeaderSize {
|
||||||
|
return nil, ErrHeaderTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
// Read JSON Header
|
// Read JSON Header
|
||||||
headerBytes := make([]byte, headerLength)
|
headerBytes := make([]byte, headerLength)
|
||||||
if _, err := io.ReadFull(buf, headerBytes); err != nil {
|
if _, err := io.ReadFull(reader, headerBytes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var header map[string]interface{}
|
var header map[string]interface{}
|
||||||
|
|
@ -119,11 +201,23 @@ func Decode(data []byte, magicNumber string) (*Trix, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read Payload
|
// Read Payload
|
||||||
payload, err := io.ReadAll(buf)
|
payload, err := io.ReadAll(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify checksum if it exists in the header
|
||||||
|
if checksum, ok := header["checksum"].(string); ok {
|
||||||
|
algo, ok := header["checksum_algo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("trix: checksum algorithm not found in header")
|
||||||
|
}
|
||||||
|
expectedChecksum := crypt.NewService().Hash(crypt.HashType(algo), string(payload))
|
||||||
|
if checksum != expectedChecksum {
|
||||||
|
return nil, ErrChecksumMismatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &Trix{
|
return &Trix{
|
||||||
Header: header,
|
Header: header,
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
|
|
@ -131,12 +225,13 @@ func Decode(data []byte, magicNumber string) (*Trix, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pack applies the In method of all attached sigils to the payload.
|
// Pack applies the In method of all attached sigils to the payload.
|
||||||
|
// It modifies the Trix struct in place.
|
||||||
func (t *Trix) Pack() error {
|
func (t *Trix) Pack() error {
|
||||||
for _, sigil := range t.Sigils {
|
for _, sigilName := range t.InSigils {
|
||||||
if sigil == nil {
|
sigil, err := enchantrix.NewSigil(sigilName)
|
||||||
return ErrNilSigil
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
t.Payload, err = sigil.In(t.Payload)
|
t.Payload, err = sigil.In(t.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -146,13 +241,18 @@ func (t *Trix) Pack() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unpack applies the Out method of all sigils in reverse order.
|
// Unpack applies the Out method of all sigils in reverse order.
|
||||||
|
// It modifies the Trix struct in place.
|
||||||
func (t *Trix) Unpack() error {
|
func (t *Trix) Unpack() error {
|
||||||
for i := len(t.Sigils) - 1; i >= 0; i-- {
|
sigilNames := t.OutSigils
|
||||||
sigil := t.Sigils[i]
|
if len(sigilNames) == 0 {
|
||||||
if sigil == nil {
|
sigilNames = t.InSigils
|
||||||
return ErrNilSigil
|
}
|
||||||
|
for i := len(sigilNames) - 1; i >= 0; i-- {
|
||||||
|
sigilName := sigilNames[i]
|
||||||
|
sigil, err := enchantrix.NewSigil(sigilName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
t.Payload, err = sigil.Out(t.Payload)
|
t.Payload, err = sigil.Out(t.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -160,21 +260,3 @@ func (t *Trix) Unpack() error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReverseSigil is an example Sigil that reverses the bytes of the payload.
|
|
||||||
type ReverseSigil struct{}
|
|
||||||
|
|
||||||
// In reverses the bytes of the data.
|
|
||||||
func (s *ReverseSigil) In(data []byte) ([]byte, error) {
|
|
||||||
reversed := make([]byte, len(data))
|
|
||||||
for i, j := 0, len(data)-1; i < len(data); i, j = i+1, j-1 {
|
|
||||||
reversed[i] = data[j]
|
|
||||||
}
|
|
||||||
return reversed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out reverses the bytes of the data.
|
|
||||||
func (s *ReverseSigil) Out(data []byte) ([]byte, error) {
|
|
||||||
// Reversing the bytes again restores the original data.
|
|
||||||
return s.In(data)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,47 @@
|
||||||
package trix
|
package trix_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
"github.com/Snider/Enchantrix/pkg/trix"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// failWriter is an io.Writer that fails on the nth write call.
|
||||||
|
type failWriter struct {
|
||||||
|
failOnCall int
|
||||||
|
callCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *failWriter) Write(p []byte) (n int, err error) {
|
||||||
|
m.callCount++
|
||||||
|
if m.callCount == m.failOnCall {
|
||||||
|
return 0, errors.New("write error")
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// failReader is an io.Reader that fails on the nth read call.
|
||||||
|
type failReader struct {
|
||||||
|
failOnCall int
|
||||||
|
callCount int
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *failReader) Read(p []byte) (n int, err error) {
|
||||||
|
m.callCount++
|
||||||
|
if m.callCount == m.failOnCall {
|
||||||
|
return 0, errors.New("read error")
|
||||||
|
}
|
||||||
|
return m.reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
// TestTrixEncodeDecode_Good tests the ideal "happy path" scenario for encoding and decoding.
|
// TestTrixEncodeDecode_Good tests the ideal "happy path" scenario for encoding and decoding.
|
||||||
func TestTrixEncodeDecode_Good(t *testing.T) {
|
func TestTrixEncodeDecode_Good(t *testing.T) {
|
||||||
header := map[string]interface{}{
|
header := map[string]interface{}{
|
||||||
|
|
@ -18,37 +51,37 @@ func TestTrixEncodeDecode_Good(t *testing.T) {
|
||||||
"created_at": "2025-10-30T12:00:00Z",
|
"created_at": "2025-10-30T12:00:00Z",
|
||||||
}
|
}
|
||||||
payload := []byte("This is a secret message.")
|
payload := []byte("This is a secret message.")
|
||||||
trix := &Trix{Header: header, Payload: payload}
|
trixOb := &trix.Trix{Header: header, Payload: payload}
|
||||||
magicNumber := "TRIX"
|
magicNumber := "TRIX"
|
||||||
|
|
||||||
encoded, err := Encode(trix, magicNumber)
|
encoded, err := trix.Encode(trixOb, magicNumber, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
decoded, err := Decode(encoded, magicNumber)
|
decoded, err := trix.Decode(encoded, magicNumber, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, reflect.DeepEqual(trix.Header, decoded.Header))
|
assert.True(t, reflect.DeepEqual(trixOb.Header, decoded.Header))
|
||||||
assert.Equal(t, trix.Payload, decoded.Payload)
|
assert.Equal(t, trixOb.Payload, decoded.Payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTrixEncodeDecode_Bad tests expected failure scenarios with well-formed but invalid inputs.
|
// TestTrixEncodeDecode_Bad tests expected failure scenarios with well-formed but invalid inputs.
|
||||||
func TestTrixEncodeDecode_Bad(t *testing.T) {
|
func TestTrixEncodeDecode_Bad(t *testing.T) {
|
||||||
t.Run("MismatchedMagicNumber", func(t *testing.T) {
|
t.Run("MismatchedMagicNumber", func(t *testing.T) {
|
||||||
trix := &Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
trixOb := &trix.Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
||||||
encoded, err := Encode(trix, "GOOD")
|
encoded, err := trix.Encode(trixOb, "GOOD", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, err = Decode(encoded, "BAD!")
|
_, err = trix.Decode(encoded, "BAD!", nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "invalid magic number")
|
assert.Contains(t, err.Error(), "invalid magic number")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidMagicNumberLength", func(t *testing.T) {
|
t.Run("InvalidMagicNumberLength", func(t *testing.T) {
|
||||||
trix := &Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
trixOb := &trix.Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
||||||
_, err := Encode(trix, "TOOLONG")
|
_, err := trix.Encode(trixOb, "TOOLONG", nil)
|
||||||
assert.EqualError(t, err, "trix: magic number must be 4 bytes long")
|
assert.EqualError(t, err, "trix: magic number must be 4 bytes long")
|
||||||
|
|
||||||
_, err = Decode([]byte{}, "SHORT")
|
_, err = trix.Decode([]byte{}, "SHORT", nil)
|
||||||
assert.EqualError(t, err, "trix: magic number must be 4 bytes long")
|
assert.EqualError(t, err, "trix: magic number must be 4 bytes long")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -57,11 +90,24 @@ func TestTrixEncodeDecode_Bad(t *testing.T) {
|
||||||
header := map[string]interface{}{
|
header := map[string]interface{}{
|
||||||
"unsupported": make(chan int), // Channels cannot be JSON-encoded
|
"unsupported": make(chan int), // Channels cannot be JSON-encoded
|
||||||
}
|
}
|
||||||
trix := &Trix{Header: header, Payload: []byte("payload")}
|
trixOb := &trix.Trix{Header: header, Payload: []byte("payload")}
|
||||||
_, err := Encode(trix, "TRIX")
|
_, err := trix.Encode(trixOb, "TRIX", nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "json: unsupported type")
|
assert.Contains(t, err.Error(), "json: unsupported type")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("HeaderTooLarge", func(t *testing.T) {
|
||||||
|
data := make([]byte, trix.MaxHeaderSize+10)
|
||||||
|
trixOb := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{"large": string(data)},
|
||||||
|
Payload: []byte("payload"),
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(trixOb, "TRIX", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = trix.Decode(encoded, "TRIX", nil)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrHeaderTooLarge)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTrixEncodeDecode_Ugly tests malicious or malformed inputs designed to cause crashes or panics.
|
// TestTrixEncodeDecode_Ugly tests malicious or malformed inputs designed to cause crashes or panics.
|
||||||
|
|
@ -71,43 +117,52 @@ func TestTrixEncodeDecode_Ugly(t *testing.T) {
|
||||||
t.Run("CorruptedHeaderLength", func(t *testing.T) {
|
t.Run("CorruptedHeaderLength", func(t *testing.T) {
|
||||||
// Manually construct a byte slice where the header length is larger than the actual data.
|
// Manually construct a byte slice where the header length is larger than the actual data.
|
||||||
var buf []byte
|
var buf []byte
|
||||||
buf = append(buf, []byte(magicNumber)...) // Magic Number
|
buf = append(buf, []byte(magicNumber)...) // Magic Number
|
||||||
buf = append(buf, byte(Version)) // Version
|
buf = append(buf, byte(trix.Version)) // Version
|
||||||
// Header length of 1000, but the header is only 2 bytes long.
|
|
||||||
buf = append(buf, []byte{0, 0, 3, 232}...) // BigEndian representation of 1000
|
buf = append(buf, []byte{0, 0, 3, 232}...) // BigEndian representation of 1000
|
||||||
buf = append(buf, []byte("{}")...) // A minimal valid JSON header
|
buf = append(buf, []byte("{}")...) // A minimal valid JSON header
|
||||||
buf = append(buf, []byte("payload")...)
|
buf = append(buf, []byte("payload")...)
|
||||||
|
|
||||||
_, err := Decode(buf, magicNumber)
|
_, err := trix.Decode(buf, magicNumber, nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, err, io.ErrUnexpectedEOF)
|
assert.Equal(t, err, io.ErrUnexpectedEOF)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidVersion", func(t *testing.T) {
|
||||||
|
var buf []byte
|
||||||
|
buf = append(buf, []byte(magicNumber)...)
|
||||||
|
buf = append(buf, byte(99)) // Invalid version
|
||||||
|
buf = append(buf, []byte{0, 0, 0, 2}...)
|
||||||
|
buf = append(buf, []byte("{}")...)
|
||||||
|
buf = append(buf, []byte("payload")...)
|
||||||
|
|
||||||
|
_, err := trix.Decode(buf, magicNumber, nil)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrInvalidVersion)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("DataTooShort", func(t *testing.T) {
|
t.Run("DataTooShort", func(t *testing.T) {
|
||||||
// Data is too short to contain even the magic number.
|
|
||||||
data := []byte("BAD")
|
data := []byte("BAD")
|
||||||
_, err := Decode(data, magicNumber)
|
_, err := trix.Decode(data, magicNumber, nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("EmptyPayload", func(t *testing.T) {
|
t.Run("EmptyPayload", func(t *testing.T) {
|
||||||
data := []byte{}
|
data := []byte{}
|
||||||
_, err := Decode(data, magicNumber)
|
_, err := trix.Decode(data, magicNumber, nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("FuzzedJSON", func(t *testing.T) {
|
t.Run("FuzzedJSON", func(t *testing.T) {
|
||||||
// A header that is technically valid but contains unexpected types.
|
|
||||||
header := map[string]interface{}{
|
header := map[string]interface{}{
|
||||||
"payload": map[string]interface{}{"nested": 123},
|
"payload": map[string]interface{}{"nested": 123},
|
||||||
}
|
}
|
||||||
payload := []byte("some data")
|
payload := []byte("some data")
|
||||||
trix := &Trix{Header: header, Payload: payload}
|
trixOb := &trix.Trix{Header: header, Payload: payload}
|
||||||
|
|
||||||
encoded, err := Encode(trix, magicNumber)
|
encoded, err := trix.Encode(trixOb, magicNumber, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
decoded, err := Decode(encoded, magicNumber)
|
decoded, err := trix.Decode(encoded, magicNumber, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, decoded)
|
assert.NotNil(t, decoded)
|
||||||
})
|
})
|
||||||
|
|
@ -115,58 +170,187 @@ func TestTrixEncodeDecode_Ugly(t *testing.T) {
|
||||||
|
|
||||||
// --- Sigil Tests ---
|
// --- Sigil Tests ---
|
||||||
|
|
||||||
// FailingSigil is a helper for testing sigils that intentionally fail.
|
|
||||||
type FailingSigil struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FailingSigil) In(data []byte) ([]byte, error) {
|
|
||||||
return nil, s.err
|
|
||||||
}
|
|
||||||
func (s *FailingSigil) Out(data []byte) ([]byte, error) {
|
|
||||||
return nil, s.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPackUnpack_Good(t *testing.T) {
|
func TestPackUnpack_Good(t *testing.T) {
|
||||||
originalPayload := []byte("hello world")
|
originalPayload := []byte("hello world")
|
||||||
trix := &Trix{
|
trixOb := &trix.Trix{
|
||||||
Header: map[string]interface{}{},
|
Header: map[string]interface{}{},
|
||||||
Payload: originalPayload,
|
Payload: originalPayload,
|
||||||
Sigils: []Sigil{&ReverseSigil{}, &ReverseSigil{}}, // Double reverse should be original
|
InSigils: []string{"reverse", "reverse"}, // Double reverse should be original
|
||||||
}
|
}
|
||||||
|
|
||||||
err := trix.Pack()
|
err := trixOb.Pack()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, originalPayload, trix.Payload) // Should be back to the original
|
assert.Equal(t, originalPayload, trixOb.Payload)
|
||||||
|
|
||||||
err = trix.Unpack()
|
err = trixOb.Unpack()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, originalPayload, trix.Payload) // Should be back to the original again
|
assert.Equal(t, originalPayload, trixOb.Payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackUnpack_Bad(t *testing.T) {
|
func TestPackUnpack_Bad(t *testing.T) {
|
||||||
expectedErr := errors.New("sigil failed")
|
trixOb := &trix.Trix{
|
||||||
trix := &Trix{
|
Header: map[string]interface{}{},
|
||||||
Header: map[string]interface{}{},
|
Payload: []byte("some data"),
|
||||||
Payload: []byte("some data"),
|
InSigils: []string{"reverse", "invalid-sigil-name"},
|
||||||
Sigils: []Sigil{&ReverseSigil{}, &FailingSigil{err: expectedErr}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := trix.Pack()
|
err := trixOb.Pack()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown sigil name")
|
||||||
|
|
||||||
|
trixOb.InSigils = []string{"hex"}
|
||||||
|
trixOb.Payload = []byte("not hex")
|
||||||
|
err = trixOb.Unpack()
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
trixOb.InSigils = []string{"json"}
|
||||||
|
trixOb.Payload = []byte("not json")
|
||||||
|
err = trixOb.Pack()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, expectedErr, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackUnpack_Ugly(t *testing.T) {
|
func TestPackUnpack_Ugly(t *testing.T) {
|
||||||
t.Run("NilSigil", func(t *testing.T) {
|
trixOb := &trix.Trix{
|
||||||
trix := &Trix{
|
Header: map[string]interface{}{},
|
||||||
Header: map[string]interface{}{},
|
Payload: nil, // Nil payload
|
||||||
Payload: []byte("some data"),
|
InSigils: []string{"reverse"},
|
||||||
Sigils: []Sigil{nil},
|
}
|
||||||
}
|
err := trixOb.Pack()
|
||||||
|
assert.NoError(t, err) // Should handle nil payload gracefully
|
||||||
|
|
||||||
err := trix.Pack()
|
err = trixOb.Unpack()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Checksum Tests ---
|
||||||
|
|
||||||
|
func TestChecksum_Good(t *testing.T) {
|
||||||
|
trixOb := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("hello world"),
|
||||||
|
ChecksumAlgo: crypt.SHA256,
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(trixOb, "CHCK", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
decoded, err := trix.Decode(encoded, "CHCK", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, trixOb.Payload, decoded.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChecksum_Bad(t *testing.T) {
|
||||||
|
trixOb := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("hello world"),
|
||||||
|
ChecksumAlgo: crypt.SHA256,
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(trixOb, "CHCK", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
encoded[len(encoded)-1] = 0 // Tamper with the payload
|
||||||
|
|
||||||
|
_, err = trix.Decode(encoded, "CHCK", nil)
|
||||||
|
assert.ErrorIs(t, err, trix.ErrChecksumMismatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChecksum_Ugly(t *testing.T) {
|
||||||
|
t.Run("MissingAlgoInHeader", func(t *testing.T) {
|
||||||
|
trixOb := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{},
|
||||||
|
Payload: []byte("hello world"),
|
||||||
|
ChecksumAlgo: crypt.SHA256,
|
||||||
|
}
|
||||||
|
encoded, err := trix.Encode(trixOb, "UGLY", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
decoded, err := trix.Decode(encoded, "UGLY", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
delete(decoded.Header, "checksum_algo")
|
||||||
|
|
||||||
|
tamperedEncoded, err := trix.Encode(decoded, "UGLY", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = trix.Decode(tamperedEncoded, "UGLY", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fuzz Tests ---
|
||||||
|
|
||||||
|
func FuzzDecode(f *testing.F) {
|
||||||
|
validTrix := &trix.Trix{
|
||||||
|
Header: map[string]interface{}{"content_type": "text/plain"},
|
||||||
|
Payload: []byte("hello world"),
|
||||||
|
}
|
||||||
|
validEncoded, _ := trix.Encode(validTrix, "FUZZ", nil)
|
||||||
|
f.Add(validEncoded)
|
||||||
|
|
||||||
|
var buf []byte
|
||||||
|
buf = append(buf, []byte("UGLY")...)
|
||||||
|
buf = append(buf, byte(trix.Version))
|
||||||
|
buf = append(buf, []byte{0, 0, 3, 232}...)
|
||||||
|
buf = append(buf, []byte("{}")...)
|
||||||
|
buf = append(buf, []byte("payload")...)
|
||||||
|
f.Add(buf)
|
||||||
|
|
||||||
|
f.Add([]byte("short"))
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
_, _ = trix.Decode(data, "FUZZ", nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode_WriteErrors(t *testing.T) {
|
||||||
|
trixOb := &trix.Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
||||||
|
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
t.Run(fmt.Sprintf("fail on write call %d", i), func(t *testing.T) {
|
||||||
|
writer := &failWriter{failOnCall: i}
|
||||||
|
_, err := trix.Encode(trixOb, "TRIX", writer)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for successful return with external writer
|
||||||
|
t.Run("SuccessfulExternalWrite", func(t *testing.T) {
|
||||||
|
writer := &failWriter{}
|
||||||
|
_, err := trix.Encode(trixOb, "TRIX", writer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode_ReadErrors(t *testing.T) {
|
||||||
|
trixOb := &trix.Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
||||||
|
encoded, err := trix.Encode(trixOb, "TRIX", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
t.Run(fmt.Sprintf("fail on read call %d", i), func(t *testing.T) {
|
||||||
|
reader := &failReader{failOnCall: i, reader: bytes.NewReader(encoded)}
|
||||||
|
_, err := trix.Decode(encoded, "TRIX", reader)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("JSONUnmarshalError", func(t *testing.T) {
|
||||||
|
// Manually construct a byte slice with an invalid JSON header.
|
||||||
|
var buf []byte
|
||||||
|
buf = append(buf, []byte("TRIX")...)
|
||||||
|
buf = append(buf, byte(trix.Version))
|
||||||
|
buf = append(buf, []byte{0, 0, 0, 5}...)
|
||||||
|
buf = append(buf, []byte("{")...)
|
||||||
|
buf = append(buf, []byte("payload")...)
|
||||||
|
|
||||||
|
_, err := trix.Decode(buf, "TRIX", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ChecksumMissingAlgo", func(t *testing.T) {
|
||||||
|
trixOb := &trix.Trix{Header: map[string]interface{}{"checksum": "abc"}, Payload: []byte("payload")}
|
||||||
|
encoded, err := trix.Encode(trixOb, "TRIX", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = trix.Decode(encoded, "TRIX", nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, ErrNilSigil, err)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
372
rfcs/RFC-0001-Pre-Obfuscation-Layer.md
Normal file
372
rfcs/RFC-0001-Pre-Obfuscation-Layer.md
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
# 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. Future Work
|
||||||
|
|
||||||
|
- [ ] Hardware-accelerated obfuscation implementations
|
||||||
|
- [ ] Additional obfuscator algorithms (block-based, etc.)
|
||||||
|
- [ ] Formal side-channel resistance analysis
|
||||||
|
- [ ] Integration benchmarks with different AEAD ciphers
|
||||||
|
- [ ] WASM compilation for browser environments
|
||||||
|
|
||||||
|
## 11. 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
|
||||||
433
rfcs/RFC-0002-Trix-Container-Format.md
Normal file
433
rfcs/RFC-0002-Trix-Container-Format.md
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
# 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 Registered Magic Numbers
|
||||||
|
|
||||||
|
The following magic numbers are registered for specific applications:
|
||||||
|
|
||||||
|
| Magic | Application | Description |
|
||||||
|
|-------|-------------|-------------|
|
||||||
|
| `SMSG` | Borg | Encrypted message/media container |
|
||||||
|
| `STIM` | Borg | Encrypted TIM container bundle |
|
||||||
|
| `STMF` | Borg | Secure To-Me Form (encrypted form data) |
|
||||||
|
| `TRIX` | Borg | Encrypted DataNode archive |
|
||||||
|
|
||||||
|
### 8.3 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. Future Work
|
||||||
|
|
||||||
|
- [ ] Media type registration (`application/x-trix`, `application/x-smsg`, etc.)
|
||||||
|
- [ ] Formal magic number registry with registration process
|
||||||
|
- [ ] Streaming encoding/decoding for large payloads
|
||||||
|
- [ ] Header compression for bandwidth-constrained environments
|
||||||
|
- [ ] Sub-container nesting specification (Trix within Trix)
|
||||||
|
|
||||||
|
## 12. 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
|
||||||
556
rfcs/RFC-0003-Sigil-Transformation-Framework.md
Normal file
556
rfcs/RFC-0003-Sigil-Transformation-Framework.md
Normal file
|
|
@ -0,0 +1,556 @@
|
||||||
|
# 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 Encryption Sigils
|
||||||
|
|
||||||
|
Encryption sigils provide authenticated encryption using AEAD ciphers.
|
||||||
|
|
||||||
|
#### 5.5.1 ChaCha20-Poly1305 Sigil
|
||||||
|
|
||||||
|
Encrypts data using XChaCha20-Poly1305 authenticated encryption.
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Name | `chacha20poly1305` |
|
||||||
|
| Category | Reversible |
|
||||||
|
| Key size | 32 bytes |
|
||||||
|
| Nonce size | 24 bytes (XChaCha variant) |
|
||||||
|
| Tag size | 16 bytes |
|
||||||
|
| In | Encrypt (generates nonce, prepends to output) |
|
||||||
|
| Out | Decrypt (extracts nonce from input prefix) |
|
||||||
|
|
||||||
|
**Critical Implementation Detail**: The nonce is embedded IN the ciphertext output, not transmitted separately:
|
||||||
|
|
||||||
|
```
|
||||||
|
In(plaintext) -> [24-byte nonce][ciphertext][16-byte tag]
|
||||||
|
Out(ciphertext_with_nonce) -> plaintext
|
||||||
|
```
|
||||||
|
|
||||||
|
**Construction**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
sigil, err := NewChaChaPolySigil(key) // key must be 32 bytes
|
||||||
|
ciphertext, err := sigil.In(plaintext)
|
||||||
|
plaintext, err := sigil.Out(ciphertext)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Properties**:
|
||||||
|
- Authenticated: Poly1305 MAC prevents tampering
|
||||||
|
- Confidential: ChaCha20 stream cipher
|
||||||
|
- Nonce uniqueness: Random 24-byte nonce per encryption
|
||||||
|
- No nonce management required by caller
|
||||||
|
|
||||||
|
### 5.6 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. Future Work
|
||||||
|
|
||||||
|
- [ ] AES-GCM encryption sigil for environments requiring AES
|
||||||
|
- [ ] Zstd compression sigil with configurable compression levels
|
||||||
|
- [ ] Streaming sigil interface for large data processing
|
||||||
|
- [ ] Sigil metadata interface for reporting transformation properties
|
||||||
|
- [ ] WebAssembly compilation for browser-based sigil operations
|
||||||
|
- [ ] Hardware acceleration detection and utilization
|
||||||
|
|
||||||
|
## 11. 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
|
||||||
|
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
| `zstd` | Compression | Yes | Zstandard |
|
||||||
|
| `json` | Formatting | Partial | Compacts JSON |
|
||||||
|
| `json-indent` | Formatting | Partial | Pretty-prints JSON |
|
||||||
|
| `chacha20poly1305` | Encryption | Yes | XChaCha20-Poly1305 AEAD |
|
||||||
|
| `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
|
||||||
406
rfcs/RFC-0004-LTHN-Hash-Algorithm.md
Normal file
406
rfcs/RFC-0004-LTHN-Hash-Algorithm.md
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
# RFC-0004: LTHN Quasi-Salted Hash Algorithm
|
||||||
|
|
||||||
|
**Status:** Informational
|
||||||
|
**Version:** 1.0
|
||||||
|
**Created:** 2025-01-13
|
||||||
|
**Author:** Snider
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
This document specifies the LTHN (Leet-Hash-N) quasi-salted hash algorithm, a deterministic hashing scheme that derives a salt from the input itself using character substitution and reversal. LTHN produces reproducible hashes that can be verified without storing a separate salt value, making it suitable for checksums, identifiers, and non-security-critical hashing applications.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Introduction](#1-introduction)
|
||||||
|
2. [Terminology](#2-terminology)
|
||||||
|
3. [Algorithm Specification](#3-algorithm-specification)
|
||||||
|
4. [Character Substitution Map](#4-character-substitution-map)
|
||||||
|
5. [Verification](#5-verification)
|
||||||
|
6. [Use Cases](#6-use-cases)
|
||||||
|
7. [Security Considerations](#7-security-considerations)
|
||||||
|
8. [Implementation Requirements](#8-implementation-requirements)
|
||||||
|
9. [Test Vectors](#9-test-vectors)
|
||||||
|
10. [References](#10-references)
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
Traditional salted hashing requires storing a random salt value alongside the hash. This provides protection against rainbow table attacks but requires additional storage and management.
|
||||||
|
|
||||||
|
LTHN takes a different approach: the salt is derived deterministically from the input itself through a transformation that:
|
||||||
|
|
||||||
|
1. Reverses the input string
|
||||||
|
2. Applies character substitutions inspired by "leet speak" conventions
|
||||||
|
|
||||||
|
This produces a quasi-salt that varies with input content while remaining reproducible, enabling verification without salt storage.
|
||||||
|
|
||||||
|
### 1.1 Design Goals
|
||||||
|
|
||||||
|
- **Determinism**: Same input always produces same hash
|
||||||
|
- **Salt derivation**: No external salt storage required
|
||||||
|
- **Verifiability**: Hashes can be verified with only the input
|
||||||
|
- **Simplicity**: Easy to implement and understand
|
||||||
|
- **Interoperability**: Based on standard SHA-256
|
||||||
|
|
||||||
|
### 1.2 Non-Goals
|
||||||
|
|
||||||
|
LTHN is NOT designed to:
|
||||||
|
- Replace proper password hashing (use bcrypt, Argon2, etc.)
|
||||||
|
- Provide cryptographic security against determined attackers
|
||||||
|
- Resist preimage or collision attacks beyond SHA-256's guarantees
|
||||||
|
|
||||||
|
## 2. Terminology
|
||||||
|
|
||||||
|
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||||
|
|
||||||
|
**Input**: The original string to be hashed
|
||||||
|
**Quasi-salt**: A salt derived from the input itself
|
||||||
|
**Key map**: The character substitution table
|
||||||
|
**LTHN hash**: The final hash output
|
||||||
|
|
||||||
|
## 3. Algorithm Specification
|
||||||
|
|
||||||
|
### 3.1 Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
LTHN(input) = SHA256(input || createSalt(input))
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `||` denotes concatenation and `createSalt` is defined below.
|
||||||
|
|
||||||
|
### 3.2 Salt Creation Algorithm
|
||||||
|
|
||||||
|
```
|
||||||
|
function createSalt(input: string) -> string:
|
||||||
|
if input is empty:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
runes = input as array of Unicode code points
|
||||||
|
salt = new array of size length(runes)
|
||||||
|
|
||||||
|
for i = 0 to length(runes) - 1:
|
||||||
|
// Reverse: take character from end
|
||||||
|
char = runes[length(runes) - 1 - i]
|
||||||
|
|
||||||
|
// Apply substitution if exists in key map
|
||||||
|
if char in keyMap:
|
||||||
|
salt[i] = keyMap[char]
|
||||||
|
else:
|
||||||
|
salt[i] = char
|
||||||
|
|
||||||
|
return salt as string
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Hash Algorithm
|
||||||
|
|
||||||
|
```
|
||||||
|
function Hash(input: string) -> string:
|
||||||
|
salt = createSalt(input)
|
||||||
|
combined = input + salt
|
||||||
|
digest = SHA256(combined as UTF-8 bytes)
|
||||||
|
return hexEncode(digest)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Output Format
|
||||||
|
|
||||||
|
- Output: 64-character lowercase hexadecimal string
|
||||||
|
- Digest: 32 bytes (256 bits)
|
||||||
|
|
||||||
|
## 4. Character Substitution Map
|
||||||
|
|
||||||
|
### 4.1 Default Key Map
|
||||||
|
|
||||||
|
The default substitution map uses bidirectional "leet speak" style mappings:
|
||||||
|
|
||||||
|
| Input | Output | Description |
|
||||||
|
|-------|--------|-------------|
|
||||||
|
| `o` | `0` | Letter O to zero |
|
||||||
|
| `l` | `1` | Letter L to one |
|
||||||
|
| `e` | `3` | Letter E to three |
|
||||||
|
| `a` | `4` | Letter A to four |
|
||||||
|
| `s` | `z` | Letter S to Z |
|
||||||
|
| `t` | `7` | Letter T to seven |
|
||||||
|
| `0` | `o` | Zero to letter O |
|
||||||
|
| `1` | `l` | One to letter L |
|
||||||
|
| `3` | `e` | Three to letter E |
|
||||||
|
| `4` | `a` | Four to letter A |
|
||||||
|
| `7` | `t` | Seven to letter T |
|
||||||
|
|
||||||
|
Note: The mapping is NOT fully symmetric. `z` does NOT map back to `s`.
|
||||||
|
|
||||||
|
### 4.2 Key Map as Code
|
||||||
|
|
||||||
|
```
|
||||||
|
keyMap = {
|
||||||
|
'o': '0',
|
||||||
|
'l': '1',
|
||||||
|
'e': '3',
|
||||||
|
'a': '4',
|
||||||
|
's': 'z',
|
||||||
|
't': '7',
|
||||||
|
'0': 'o',
|
||||||
|
'1': 'l',
|
||||||
|
'3': 'e',
|
||||||
|
'4': 'a',
|
||||||
|
'7': 't'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Custom Key Maps
|
||||||
|
|
||||||
|
Implementations MAY support custom key maps. When using custom maps:
|
||||||
|
|
||||||
|
- Document the custom map clearly
|
||||||
|
- Ensure bidirectional mappings are intentional
|
||||||
|
- Consider character set implications (Unicode vs. ASCII)
|
||||||
|
|
||||||
|
## 5. Verification
|
||||||
|
|
||||||
|
### 5.1 Verification Algorithm
|
||||||
|
|
||||||
|
```
|
||||||
|
function Verify(input: string, expectedHash: string) -> bool:
|
||||||
|
actualHash = Hash(input)
|
||||||
|
return constantTimeCompare(actualHash, expectedHash)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Properties
|
||||||
|
|
||||||
|
- Verification requires only the input and hash
|
||||||
|
- No salt storage or retrieval necessary
|
||||||
|
- Same input always produces same hash
|
||||||
|
|
||||||
|
## 6. Use Cases
|
||||||
|
|
||||||
|
### 6.1 Recommended Uses
|
||||||
|
|
||||||
|
| Use Case | Suitability | Notes |
|
||||||
|
|----------|-------------|-------|
|
||||||
|
| Content identifiers | Good | Deterministic, reproducible |
|
||||||
|
| Cache keys | Good | Same content = same key |
|
||||||
|
| Deduplication | Good | Identify identical content |
|
||||||
|
| File integrity | Moderate | Use with checksum comparison |
|
||||||
|
| Non-critical checksums | Good | Simple verification |
|
||||||
|
| Rolling key derivation | Good | Time-based key rotation (see 6.3) |
|
||||||
|
|
||||||
|
### 6.2 Not Recommended Uses
|
||||||
|
|
||||||
|
| Use Case | Reason |
|
||||||
|
|----------|--------|
|
||||||
|
| Password storage | Use bcrypt, Argon2, or scrypt instead |
|
||||||
|
| Authentication tokens | Use HMAC or proper MACs |
|
||||||
|
| Digital signatures | Use proper signature schemes |
|
||||||
|
| Security-critical integrity | Use HMAC-SHA256 |
|
||||||
|
|
||||||
|
### 6.3 Rolling Key Derivation Pattern
|
||||||
|
|
||||||
|
LTHN is well-suited for deriving time-based rolling keys for streaming media or time-limited access control. The pattern combines a time period with user credentials:
|
||||||
|
|
||||||
|
```
|
||||||
|
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.1 Cadence Formats
|
||||||
|
|
||||||
|
| Cadence | Period Format | Example | Window |
|
||||||
|
|---------|---------------|---------|--------|
|
||||||
|
| daily | YYYY-MM-DD | "2026-01-13" | 24 hours |
|
||||||
|
| 12h | YYYY-MM-DD-AM/PM | "2026-01-13-AM" | 12 hours |
|
||||||
|
| 6h | YYYY-MM-DD-HH | "2026-01-13-00" | 6 hours (00, 06, 12, 18) |
|
||||||
|
| 1h | YYYY-MM-DD-HH | "2026-01-13-15" | 1 hour |
|
||||||
|
|
||||||
|
#### 6.3.2 Rolling Window Implementation
|
||||||
|
|
||||||
|
For graceful key transitions, implementations should support a rolling window:
|
||||||
|
|
||||||
|
```
|
||||||
|
function GetRollingPeriods(cadence: string) -> (current: string, next: string):
|
||||||
|
now = currentTime()
|
||||||
|
current = formatPeriod(now, cadence)
|
||||||
|
next = formatPeriod(now + periodDuration(cadence), cadence)
|
||||||
|
return (current, next)
|
||||||
|
```
|
||||||
|
|
||||||
|
Content encrypted with rolling keys includes wrapped CEKs (Content Encryption Keys) for both current and next periods, allowing decryption during period transitions.
|
||||||
|
|
||||||
|
#### 6.3.3 CEK Wrapping
|
||||||
|
|
||||||
|
```
|
||||||
|
// Wrap CEK for distribution
|
||||||
|
For each period in [current, next]:
|
||||||
|
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
|
||||||
|
wrappedCEK = ChaCha20Poly1305_Encrypt(CEK, streamKey)
|
||||||
|
store (period, wrappedCEK) in header
|
||||||
|
|
||||||
|
// Unwrap CEK for playback
|
||||||
|
For each (period, wrappedCEK) in header:
|
||||||
|
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
|
||||||
|
CEK = ChaCha20Poly1305_Decrypt(wrappedCEK, streamKey)
|
||||||
|
if success: return CEK
|
||||||
|
return error("no valid key for current period")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Security Considerations
|
||||||
|
|
||||||
|
### 7.1 Not a Password Hash
|
||||||
|
|
||||||
|
LTHN MUST NOT be used for password hashing because:
|
||||||
|
|
||||||
|
- No work factor (bcrypt, Argon2 have tunable cost)
|
||||||
|
- No random salt (predictable salt derivation)
|
||||||
|
- Fast to compute (enables brute force)
|
||||||
|
- No memory hardness (GPU/ASIC friendly)
|
||||||
|
|
||||||
|
### 7.2 Quasi-Salt Limitations
|
||||||
|
|
||||||
|
The derived salt provides limited protection:
|
||||||
|
|
||||||
|
- Salt is deterministic, not random
|
||||||
|
- Identical inputs produce identical salts
|
||||||
|
- Does not prevent rainbow tables for known inputs
|
||||||
|
- Salt derivation algorithm is public
|
||||||
|
|
||||||
|
### 7.3 SHA-256 Dependency
|
||||||
|
|
||||||
|
Security properties depend on SHA-256:
|
||||||
|
|
||||||
|
- Preimage resistance: Finding input from hash is hard
|
||||||
|
- Second preimage resistance: Finding different input with same hash is hard
|
||||||
|
- Collision resistance: Finding two inputs with same hash is hard
|
||||||
|
|
||||||
|
These properties apply to the combined `input || salt` value.
|
||||||
|
|
||||||
|
### 7.4 Timing Attacks
|
||||||
|
|
||||||
|
Verification SHOULD use constant-time comparison to prevent timing attacks:
|
||||||
|
|
||||||
|
```
|
||||||
|
function constantTimeCompare(a: string, b: string) -> bool:
|
||||||
|
if length(a) != length(b):
|
||||||
|
return false
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
for i = 0 to length(a) - 1:
|
||||||
|
result |= a[i] XOR b[i]
|
||||||
|
|
||||||
|
return result == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Implementation Requirements
|
||||||
|
|
||||||
|
Conforming implementations MUST:
|
||||||
|
|
||||||
|
1. Use SHA-256 as the underlying hash function
|
||||||
|
2. Concatenate input and salt in the order: `input || salt`
|
||||||
|
3. Use the default key map unless explicitly configured otherwise
|
||||||
|
4. Output lowercase hexadecimal encoding
|
||||||
|
5. Handle empty strings by returning SHA-256 of empty string
|
||||||
|
6. Support Unicode input (process as UTF-8 bytes after salt creation)
|
||||||
|
|
||||||
|
Conforming implementations SHOULD:
|
||||||
|
|
||||||
|
1. Provide constant-time verification
|
||||||
|
2. Support custom key maps via configuration
|
||||||
|
3. Document any deviations from the default key map
|
||||||
|
|
||||||
|
## 9. Test Vectors
|
||||||
|
|
||||||
|
### 9.1 Basic Test Cases
|
||||||
|
|
||||||
|
| Input | Salt | Combined | LTHN Hash |
|
||||||
|
|-------|------|----------|-----------|
|
||||||
|
| `""` | `""` | `""` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |
|
||||||
|
| `"a"` | `"4"` | `"a4"` | `a4a4e5c4b3b2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6` |
|
||||||
|
| `"hello"` | `"011eh"` | `"hello011eh"` | (computed) |
|
||||||
|
| `"test"` | `"7z37"` | `"test7z37"` | (computed) |
|
||||||
|
|
||||||
|
### 9.2 Character Substitution Examples
|
||||||
|
|
||||||
|
| Input | Reversed | After Substitution (Salt) |
|
||||||
|
|-------|----------|---------------------------|
|
||||||
|
| `"hello"` | `"olleh"` | `"011eh"` |
|
||||||
|
| `"test"` | `"tset"` | `"7z37"` |
|
||||||
|
| `"password"` | `"drowssap"` | `"dr0wzz4p"` |
|
||||||
|
| `"12345"` | `"54321"` | `"5ae2l"` |
|
||||||
|
|
||||||
|
### 9.3 Unicode Test Cases
|
||||||
|
|
||||||
|
| Input | Expected Behavior |
|
||||||
|
|-------|-------------------|
|
||||||
|
| `"cafe"` | Standard processing |
|
||||||
|
| `"caf`e`"` | e with accent NOT substituted (only ASCII 'e' matches) |
|
||||||
|
|
||||||
|
Note: Key map only matches exact character codes, not normalized equivalents.
|
||||||
|
|
||||||
|
## 10. API Reference
|
||||||
|
|
||||||
|
### 10.1 Go API
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/Snider/Enchantrix/pkg/crypt"
|
||||||
|
|
||||||
|
// Create crypt service
|
||||||
|
svc := crypt.NewService()
|
||||||
|
|
||||||
|
// Hash with LTHN
|
||||||
|
hash := svc.Hash(crypt.LTHN, "input string")
|
||||||
|
|
||||||
|
// Available hash types
|
||||||
|
crypt.LTHN // LTHN quasi-salted hash
|
||||||
|
crypt.SHA256 // Standard SHA-256
|
||||||
|
crypt.SHA512 // Standard SHA-512
|
||||||
|
// ... other standard algorithms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 Direct Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/Snider/Enchantrix/pkg/crypt/std/lthn"
|
||||||
|
|
||||||
|
// Direct LTHN hash
|
||||||
|
hash := lthn.Hash("input string")
|
||||||
|
|
||||||
|
// Verify hash
|
||||||
|
valid := lthn.Verify("input string", expectedHash)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Future Work
|
||||||
|
|
||||||
|
- [ ] Custom key map configuration via API
|
||||||
|
- [ ] WASM compilation for browser-based LTHN operations
|
||||||
|
- [ ] Alternative underlying hash functions (SHA-3, BLAKE3)
|
||||||
|
- [ ] Configurable salt derivation strategies
|
||||||
|
- [ ] Performance optimization for high-throughput scenarios
|
||||||
|
- [ ] Formal security analysis of rolling key pattern
|
||||||
|
|
||||||
|
## 12. References
|
||||||
|
|
||||||
|
- [FIPS 180-4] Secure Hash Standard (SHA-256)
|
||||||
|
- [RFC 4648] The Base16, Base32, and Base64 Data Encodings
|
||||||
|
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
|
||||||
|
- [Wikipedia: Leet] History and conventions of leet speak character substitution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A: Reference Implementation
|
||||||
|
|
||||||
|
A reference implementation in Go is available at:
|
||||||
|
`github.com/Snider/Enchantrix/pkg/crypt/std/lthn/lthn.go`
|
||||||
|
|
||||||
|
## Appendix B: Historical Note
|
||||||
|
|
||||||
|
The name "LTHN" derives from "Leet Hash N" or "Lethean" (relating to forgetfulness/oblivion in Greek mythology), referencing both the leet-speak character substitutions and the one-way nature of hash functions.
|
||||||
|
|
||||||
|
## Appendix C: Comparison with Other Schemes
|
||||||
|
|
||||||
|
| Scheme | Salt | Work Factor | Suitable for Passwords |
|
||||||
|
|--------|------|-------------|------------------------|
|
||||||
|
| LTHN | Derived | None | No |
|
||||||
|
| SHA-256 | None | None | No |
|
||||||
|
| HMAC-SHA256 | Key-based | None | No |
|
||||||
|
| bcrypt | Random | Yes | Yes |
|
||||||
|
| Argon2 | Random | Yes | Yes |
|
||||||
|
| scrypt | Random | Yes | Yes |
|
||||||
|
|
||||||
|
## Appendix D: Changelog
|
||||||
|
|
||||||
|
- **1.0** (2025-01-13): Initial specification
|
||||||
Loading…
Add table
Reference in a new issue