feat: Implement pre-encryption sigil packing for enhanced security
This commit introduces a more secure sigil workflow by separating the transformation logic from the serialization logic. - The `Trix` struct now has `Pack()` and `Unpack()` methods to apply sigil transformations directly to the payload. - The `Encode()` and `Decode()` functions are now solely responsible for serialization, no longer handling sigil logic. - The recommended workflow is to `Pack()` the data (obfuscating it), then encrypt it, and finally `Encode()` the container. This prevents cleartext from being sent to the CPU's encryption instructions. This change enhances security by adding a user-defined obfuscation layer before the encryption process begins, true to the spirit of Enchantrix.
This commit is contained in:
parent
e21c910f91
commit
519e5edd68
18 changed files with 541 additions and 402 deletions
12
Taskfile.yml
Normal file
12
Taskfile.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
version: '3'
|
||||
|
||||
tasks:
|
||||
test:
|
||||
desc: "Run all tests"
|
||||
cmds:
|
||||
- go test -v ./...
|
||||
|
||||
build:
|
||||
desc: "Build the project"
|
||||
cmds:
|
||||
- go build -v ./...
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package crypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
payload := "hello"
|
||||
hash := Hash(LTHN, payload)
|
||||
assert.NotEmpty(t, hash)
|
||||
}
|
||||
|
||||
func TestLuhn(t *testing.T) {
|
||||
assert.True(t, Luhn("79927398713"))
|
||||
assert.False(t, Luhn("79927398714"))
|
||||
}
|
||||
|
||||
func TestFletcher16(t *testing.T) {
|
||||
assert.Equal(t, uint16(0xC8F0), Fletcher16("abcde"))
|
||||
assert.Equal(t, uint16(0x2057), Fletcher16("abcdef"))
|
||||
assert.Equal(t, uint16(0x0627), Fletcher16("abcdefgh"))
|
||||
}
|
||||
|
||||
func TestFletcher32(t *testing.T) {
|
||||
expected := uint32(0xF04FC729)
|
||||
actual := Fletcher32("abcde")
|
||||
fmt.Printf("Fletcher32('abcde'): expected: %x, actual: %x\n", expected, actual)
|
||||
assert.Equal(t, expected, actual)
|
||||
|
||||
expected = uint32(0x56502D2A)
|
||||
actual = Fletcher32("abcdef")
|
||||
fmt.Printf("Fletcher32('abcdef'): expected: %x, actual: %x\n", expected, actual)
|
||||
assert.Equal(t, expected, actual)
|
||||
|
||||
expected = uint32(0xEBE19591)
|
||||
actual = Fletcher32("abcdefgh")
|
||||
fmt.Printf("Fletcher32('abcdefgh'): expected: %x, actual: %x\n", expected, actual)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestFletcher64(t *testing.T) {
|
||||
assert.Equal(t, uint64(0xc8c6c527646362c6), Fletcher64("abcde"))
|
||||
assert.Equal(t, uint64(0xc8c72b276463c8c6), Fletcher64("abcdef"))
|
||||
assert.Equal(t, uint64(0x312e2b28cccac8c6), Fletcher64("abcdefgh"))
|
||||
}
|
||||
|
||||
func TestRootFS(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "enchantrix-crypt-test")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = 1
|
||||
}
|
||||
|
||||
fs := NewRootFS(tempDir, key)
|
||||
err = fs.Write("test.txt", []byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, err := fs.Read("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("hello"), data)
|
||||
}
|
||||
|
|
@ -6,80 +6,83 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/Snider/Enchantrix/chachapoly"
|
||||
"github.com/Snider/Enchantrix/trix"
|
||||
"github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly"
|
||||
"github.com/Snider/Enchantrix/pkg/trix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Original plaintext
|
||||
// 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. Encrypt the data using the chachapoly package
|
||||
// The ciphertext from chachapoly includes the nonce.
|
||||
ciphertext, err := chachapoly.Encrypt(plaintext, key)
|
||||
// 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
|
||||
|
||||
// For the .trix header, we need to separate the nonce from the ciphertext.
|
||||
// chacha20poly1305.NewX nonce size is 24 bytes.
|
||||
// 5. Add encryption metadata to the header
|
||||
nonce := ciphertext[:24]
|
||||
actualCiphertext := ciphertext[24:]
|
||||
|
||||
// 3. Create a .trix container for the encrypted data
|
||||
header := map[string]interface{}{
|
||||
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),
|
||||
}
|
||||
|
||||
trixContainer := &trix.Trix{
|
||||
Header: header,
|
||||
Payload: actualCiphertext,
|
||||
}
|
||||
|
||||
// 4. Encode the .trix container into its binary format
|
||||
encodedTrix, err := trix.Encode(trixContainer)
|
||||
// 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.")
|
||||
|
||||
// 5. Decode the .trix container to retrieve the encrypted data
|
||||
decodedTrix, err := trix.Decode(encodedTrix)
|
||||
// --- DECODING ---
|
||||
|
||||
// 7. Decode the .trix container
|
||||
decodedTrix, err := trix.Decode(encodedTrix, magicNumber)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode .trix container: %v", err)
|
||||
}
|
||||
|
||||
// 6. Reassemble the ciphertext (nonce + payload) and decrypt
|
||||
retrievedNonceStr, ok := decodedTrix.Header["nonce"].(string)
|
||||
if !ok {
|
||||
log.Fatalf("Nonce not found or not a string in header")
|
||||
}
|
||||
retrievedNonce, err := base64.StdEncoding.DecodeString(retrievedNonceStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode nonce: %v", err)
|
||||
}
|
||||
retrievedCiphertext := append(retrievedNonce, decodedTrix.Payload...)
|
||||
|
||||
decrypted, err := chachapoly.Decrypt(retrievedCiphertext, key)
|
||||
// 8. Decrypt the payload
|
||||
decryptedPayload, err := chachapoly.Decrypt(decodedTrix.Payload, key)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decrypt: %v", err)
|
||||
}
|
||||
decodedTrix.Payload = decryptedPayload
|
||||
|
||||
// 7. Verify the result
|
||||
fmt.Printf("Original plaintext: %s\n", plaintext)
|
||||
fmt.Printf("Decrypted plaintext: %s\n", decrypted)
|
||||
// 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)
|
||||
|
||||
if string(plaintext) == string(decrypted) {
|
||||
fmt.Println("\nSuccess! The message was decrypted correctly.")
|
||||
// 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 decrypted message does not match the original.")
|
||||
fmt.Println("\nFailure! The final payload does not match the original.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Enchantrix/lthn"
|
||||
"github.com/Snider/Enchantrix/pkg/crypt/std/lthn"
|
||||
)
|
||||
|
||||
// Service is the main struct for the crypt service.
|
||||
type Service struct{}
|
||||
|
||||
// NewService creates a new crypt service.
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// HashType defines the supported hashing algorithms.
|
||||
type HashType string
|
||||
|
||||
|
|
@ -27,7 +35,7 @@ const (
|
|||
// --- Hashing ---
|
||||
|
||||
// Hash computes a hash of the payload using the specified algorithm.
|
||||
func Hash(lib HashType, payload string) string {
|
||||
func (s *Service) Hash(lib HashType, payload string) string {
|
||||
switch lib {
|
||||
case LTHN:
|
||||
return lthn.Hash(payload)
|
||||
|
|
@ -51,12 +59,16 @@ func Hash(lib HashType, payload string) string {
|
|||
// --- Checksums ---
|
||||
|
||||
// Luhn validates a number using the Luhn algorithm.
|
||||
func Luhn(payload string) bool {
|
||||
func (s *Service) Luhn(payload string) bool {
|
||||
payload = strings.ReplaceAll(payload, " ", "")
|
||||
if len(payload) <= 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
sum := 0
|
||||
isSecond := false
|
||||
for i := len(payload) - 1; i >= 0; i-- {
|
||||
digit, err := strconv.Atoi(string(payload[i]))
|
||||
isSecond := len(payload)%2 == 0
|
||||
for _, r := range payload {
|
||||
digit, err := strconv.Atoi(string(r))
|
||||
if err != nil {
|
||||
return false // Contains non-digit
|
||||
}
|
||||
|
|
@ -75,7 +87,7 @@ func Luhn(payload string) bool {
|
|||
}
|
||||
|
||||
// Fletcher16 computes the Fletcher-16 checksum.
|
||||
func Fletcher16(payload string) uint16 {
|
||||
func (s *Service) Fletcher16(payload string) uint16 {
|
||||
data := []byte(payload)
|
||||
var sum1, sum2 uint16
|
||||
for _, b := range data {
|
||||
|
|
@ -86,7 +98,7 @@ func Fletcher16(payload string) uint16 {
|
|||
}
|
||||
|
||||
// Fletcher32 computes the Fletcher-32 checksum.
|
||||
func Fletcher32(payload string) uint32 {
|
||||
func (s *Service) Fletcher32(payload string) uint32 {
|
||||
data := []byte(payload)
|
||||
if len(data)%2 != 0 {
|
||||
data = append(data, 0)
|
||||
|
|
@ -102,7 +114,7 @@ func Fletcher32(payload string) uint32 {
|
|||
}
|
||||
|
||||
// Fletcher64 computes the Fletcher-64 checksum.
|
||||
func Fletcher64(payload string) uint64 {
|
||||
func (s *Service) Fletcher64(payload string) uint64 {
|
||||
data := []byte(payload)
|
||||
if len(data)%4 != 0 {
|
||||
padding := 4 - (len(data) % 4)
|
||||
|
|
@ -127,7 +139,7 @@ func Fletcher64(payload string) uint64 {
|
|||
// import "github.com/Snider/Enchantrix/openpgp"
|
||||
//
|
||||
// // EncryptPGP encrypts data for a recipient, optionally signing it.
|
||||
// func EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error {
|
||||
// func (s *Service) EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error {
|
||||
// var buf bytes.Buffer
|
||||
// err := openpgp.EncryptPGP(&buf, recipientPath, data, signerPath, signerPassphrase)
|
||||
// if err != nil {
|
||||
|
|
@ -143,6 +155,6 @@ func Fletcher64(payload string) uint64 {
|
|||
// }
|
||||
//
|
||||
// // DecryptPGP decrypts a PGP message, optionally verifying the signature.
|
||||
// func DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
|
||||
// func (s *Service) DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
|
||||
// return openpgp.DecryptPGP(recipientPath, message, passphrase, signerPath)
|
||||
// }
|
||||
109
pkg/crypt/crypt_test.go
Normal file
109
pkg/crypt/crypt_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package crypt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var service = NewService()
|
||||
|
||||
// --- Hashing Tests ---
|
||||
|
||||
func TestHash_Good(t *testing.T) {
|
||||
payload := "hello"
|
||||
// Test all supported hash types
|
||||
for _, hashType := range []HashType{LTHN, SHA512, SHA256, SHA1, MD5} {
|
||||
hash := service.Hash(hashType, payload)
|
||||
assert.NotEmpty(t, hash, "Hash should not be empty for type %s", hashType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash_Bad(t *testing.T) {
|
||||
// Using an unsupported hash type should default to SHA256
|
||||
hash := service.Hash("unsupported", "hello")
|
||||
expectedHash := service.Hash(SHA256, "hello")
|
||||
assert.Equal(t, expectedHash, hash)
|
||||
}
|
||||
|
||||
func TestHash_Ugly(t *testing.T) {
|
||||
// Test with potentially problematic inputs
|
||||
testCases := []string{
|
||||
"", // Empty string
|
||||
" ", // Whitespace
|
||||
"\x00\x01\x02\x03\x04", // Null bytes
|
||||
strings.Repeat("a", 1024*1024), // Large payload (1MB)
|
||||
"こんにちは", // Unicode characters
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
for _, hashType := range []HashType{LTHN, SHA512, SHA256, SHA1, MD5} {
|
||||
hash := service.Hash(hashType, tc)
|
||||
assert.NotEmpty(t, hash, "Hash for ugly input should not be empty for type %s", hashType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Checksum Tests ---
|
||||
|
||||
// Luhn Tests
|
||||
func TestLuhn_Good(t *testing.T) {
|
||||
assert.True(t, service.Luhn("79927398713"))
|
||||
}
|
||||
|
||||
func TestLuhn_Bad(t *testing.T) {
|
||||
assert.False(t, service.Luhn("79927398714"), "Should fail for incorrect checksum")
|
||||
assert.False(t, service.Luhn("7992739871a"), "Should fail for non-numeric input")
|
||||
}
|
||||
|
||||
func TestLuhn_Ugly(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
|
||||
// Fletcher16 Tests
|
||||
func TestFletcher16_Good(t *testing.T) {
|
||||
assert.Equal(t, uint16(0xC8F0), service.Fletcher16("abcde"))
|
||||
assert.Equal(t, uint16(0x2057), service.Fletcher16("abcdef"))
|
||||
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) {
|
||||
assert.Equal(t, uint16(0), service.Fletcher16(""), "Checksum of empty string should be 0")
|
||||
}
|
||||
|
||||
// Fletcher32 Tests
|
||||
func TestFletcher32_Good(t *testing.T) {
|
||||
assert.Equal(t, uint32(0xF04FC729), service.Fletcher32("abcde"))
|
||||
assert.Equal(t, uint32(0x56502D2A), service.Fletcher32("abcdef"))
|
||||
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) {
|
||||
assert.Equal(t, uint32(0), service.Fletcher32(""), "Checksum of empty string should be 0")
|
||||
}
|
||||
|
||||
// Fletcher64 Tests
|
||||
func TestFletcher64_Good(t *testing.T) {
|
||||
assert.Equal(t, uint64(0xc8c6c527646362c6), service.Fletcher64("abcde"))
|
||||
assert.Equal(t, uint64(0xc8c72b276463c8c6), service.Fletcher64("abcdef"))
|
||||
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) {
|
||||
assert.Equal(t, uint64(0), service.Fletcher64(""), "Checksum of empty string should be 0")
|
||||
}
|
||||
3
pkg/crypt/std/rsa/rsa.go
Normal file
3
pkg/crypt/std/rsa/rsa.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package rsa
|
||||
|
||||
// This file is a placeholder for RSA key handling functionality.
|
||||
180
pkg/trix/trix.go
Normal file
180
pkg/trix/trix.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package trix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = 2
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
|
||||
ErrInvalidVersion = errors.New("trix: invalid version")
|
||||
ErrMagicNumberLength = errors.New("trix: magic number must be 4 bytes long")
|
||||
ErrNilSigil = errors.New("trix: sigil cannot be nil")
|
||||
)
|
||||
|
||||
// Sigil defines the interface for a data transformer.
|
||||
type Sigil interface {
|
||||
In(data []byte) ([]byte, error)
|
||||
Out(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Trix represents the structure of a .trix file.
|
||||
type Trix struct {
|
||||
Header map[string]interface{}
|
||||
Payload []byte
|
||||
Sigils []Sigil `json:"-"` // Ignore Sigils during JSON marshaling
|
||||
}
|
||||
|
||||
// Encode serializes a Trix struct into the .trix binary format.
|
||||
func Encode(trix *Trix, magicNumber string) ([]byte, error) {
|
||||
if len(magicNumber) != 4 {
|
||||
return nil, ErrMagicNumberLength
|
||||
}
|
||||
|
||||
headerBytes, err := json.Marshal(trix.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headerLength := uint32(len(headerBytes))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// Write Magic Number
|
||||
if _, err := buf.WriteString(magicNumber); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write Version
|
||||
if err := buf.WriteByte(byte(Version)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write Header Length
|
||||
if err := binary.Write(buf, binary.BigEndian, headerLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write JSON Header
|
||||
if _, err := buf.Write(headerBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write Payload
|
||||
if _, err := buf.Write(trix.Payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Decode deserializes the .trix binary format into a Trix struct.
|
||||
// Note: Sigils are not stored in the format and must be re-attached by the caller.
|
||||
func Decode(data []byte, magicNumber string) (*Trix, error) {
|
||||
if len(magicNumber) != 4 {
|
||||
return nil, ErrMagicNumberLength
|
||||
}
|
||||
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
// Read and Verify Magic Number
|
||||
magic := make([]byte, 4)
|
||||
if _, err := io.ReadFull(buf, magic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if string(magic) != magicNumber {
|
||||
return nil, fmt.Errorf("%w: expected %s, got %s", ErrInvalidMagicNumber, magicNumber, string(magic))
|
||||
}
|
||||
|
||||
// Read and Verify Version
|
||||
version, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version != Version {
|
||||
return nil, ErrInvalidVersion
|
||||
}
|
||||
|
||||
// Read Header Length
|
||||
var headerLength uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &headerLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read JSON Header
|
||||
headerBytes := make([]byte, headerLength)
|
||||
if _, err := io.ReadFull(buf, headerBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var header map[string]interface{}
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read Payload
|
||||
payload, err := io.ReadAll(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Trix{
|
||||
Header: header,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Pack applies the In method of all attached sigils to the payload.
|
||||
func (t *Trix) Pack() error {
|
||||
for _, sigil := range t.Sigils {
|
||||
if sigil == nil {
|
||||
return ErrNilSigil
|
||||
}
|
||||
var err error
|
||||
t.Payload, err = sigil.In(t.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unpack applies the Out method of all sigils in reverse order.
|
||||
func (t *Trix) Unpack() error {
|
||||
for i := len(t.Sigils) - 1; i >= 0; i-- {
|
||||
sigil := t.Sigils[i]
|
||||
if sigil == nil {
|
||||
return ErrNilSigil
|
||||
}
|
||||
var err error
|
||||
t.Payload, err = sigil.Out(t.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
172
pkg/trix/trix_test.go
Normal file
172
pkg/trix/trix_test.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package trix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTrixEncodeDecode_Good tests the ideal "happy path" scenario for encoding and decoding.
|
||||
func TestTrixEncodeDecode_Good(t *testing.T) {
|
||||
header := map[string]interface{}{
|
||||
"content_type": "application/octet-stream",
|
||||
"encryption_algorithm": "chacha20poly1035",
|
||||
"nonce": "AAECAwQFBgcICQoLDA0ODxAREhMUFRY=",
|
||||
"created_at": "2025-10-30T12:00:00Z",
|
||||
}
|
||||
payload := []byte("This is a secret message.")
|
||||
trix := &Trix{Header: header, Payload: payload}
|
||||
magicNumber := "TRIX"
|
||||
|
||||
encoded, err := Encode(trix, magicNumber)
|
||||
assert.NoError(t, err)
|
||||
|
||||
decoded, err := Decode(encoded, magicNumber)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(trix.Header, decoded.Header))
|
||||
assert.Equal(t, trix.Payload, decoded.Payload)
|
||||
}
|
||||
|
||||
// TestTrixEncodeDecode_Bad tests expected failure scenarios with well-formed but invalid inputs.
|
||||
func TestTrixEncodeDecode_Bad(t *testing.T) {
|
||||
t.Run("MismatchedMagicNumber", func(t *testing.T) {
|
||||
trix := &Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
||||
encoded, err := Encode(trix, "GOOD")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = Decode(encoded, "BAD!")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid magic number")
|
||||
})
|
||||
|
||||
t.Run("InvalidMagicNumberLength", func(t *testing.T) {
|
||||
trix := &Trix{Header: map[string]interface{}{}, Payload: []byte("payload")}
|
||||
_, err := Encode(trix, "TOOLONG")
|
||||
assert.EqualError(t, err, "trix: magic number must be 4 bytes long")
|
||||
|
||||
_, err = Decode([]byte{}, "SHORT")
|
||||
assert.EqualError(t, err, "trix: magic number must be 4 bytes long")
|
||||
})
|
||||
|
||||
t.Run("MalformedHeaderJSON", func(t *testing.T) {
|
||||
// Create a Trix struct with a header that cannot be marshaled to JSON.
|
||||
header := map[string]interface{}{
|
||||
"unsupported": make(chan int), // Channels cannot be JSON-encoded
|
||||
}
|
||||
trix := &Trix{Header: header, Payload: []byte("payload")}
|
||||
_, err := Encode(trix, "TRIX")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "json: unsupported type")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTrixEncodeDecode_Ugly tests malicious or malformed inputs designed to cause crashes or panics.
|
||||
func TestTrixEncodeDecode_Ugly(t *testing.T) {
|
||||
magicNumber := "UGLY"
|
||||
|
||||
t.Run("CorruptedHeaderLength", func(t *testing.T) {
|
||||
// Manually construct a byte slice where the header length is larger than the actual data.
|
||||
var buf []byte
|
||||
buf = append(buf, []byte(magicNumber)...) // Magic Number
|
||||
buf = append(buf, byte(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("{}")...) // A minimal valid JSON header
|
||||
buf = append(buf, []byte("payload")...)
|
||||
|
||||
_, err := Decode(buf, magicNumber)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, err, io.ErrUnexpectedEOF)
|
||||
})
|
||||
|
||||
t.Run("DataTooShort", func(t *testing.T) {
|
||||
// Data is too short to contain even the magic number.
|
||||
data := []byte("BAD")
|
||||
_, err := Decode(data, magicNumber)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("EmptyPayload", func(t *testing.T) {
|
||||
data := []byte{}
|
||||
_, err := Decode(data, magicNumber)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("FuzzedJSON", func(t *testing.T) {
|
||||
// A header that is technically valid but contains unexpected types.
|
||||
header := map[string]interface{}{
|
||||
"payload": map[string]interface{}{"nested": 123},
|
||||
}
|
||||
payload := []byte("some data")
|
||||
trix := &Trix{Header: header, Payload: payload}
|
||||
|
||||
encoded, err := Encode(trix, magicNumber)
|
||||
assert.NoError(t, err)
|
||||
|
||||
decoded, err := Decode(encoded, magicNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, decoded)
|
||||
})
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
originalPayload := []byte("hello world")
|
||||
trix := &Trix{
|
||||
Header: map[string]interface{}{},
|
||||
Payload: originalPayload,
|
||||
Sigils: []Sigil{&ReverseSigil{}, &ReverseSigil{}}, // Double reverse should be original
|
||||
}
|
||||
|
||||
err := trix.Pack()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, originalPayload, trix.Payload) // Should be back to the original
|
||||
|
||||
err = trix.Unpack()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, originalPayload, trix.Payload) // Should be back to the original again
|
||||
}
|
||||
|
||||
func TestPackUnpack_Bad(t *testing.T) {
|
||||
expectedErr := errors.New("sigil failed")
|
||||
trix := &Trix{
|
||||
Header: map[string]interface{}{},
|
||||
Payload: []byte("some data"),
|
||||
Sigils: []Sigil{&ReverseSigil{}, &FailingSigil{err: expectedErr}},
|
||||
}
|
||||
|
||||
err := trix.Pack()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
}
|
||||
|
||||
func TestPackUnpack_Ugly(t *testing.T) {
|
||||
t.Run("NilSigil", func(t *testing.T) {
|
||||
trix := &Trix{
|
||||
Header: map[string]interface{}{},
|
||||
Payload: []byte("some data"),
|
||||
Sigils: []Sigil{nil},
|
||||
}
|
||||
|
||||
err := trix.Pack()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrNilSigil, err)
|
||||
})
|
||||
}
|
||||
11
rootfs.go
11
rootfs.go
|
|
@ -1,11 +0,0 @@
|
|||
package crypt
|
||||
|
||||
import "github.com/Snider/Enchantrix/rootfs"
|
||||
|
||||
// Storage is an alias for the rootfs.Storage interface.
|
||||
type Storage = rootfs.Storage
|
||||
|
||||
// NewRootFS creates a new encrypted passthrough storage system.
|
||||
func NewRootFS(root string, key []byte) Storage {
|
||||
return rootfs.NewLocalStorage(root, key)
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package rootfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Snider/Enchantrix/chachapoly"
|
||||
)
|
||||
|
||||
// LocalStorage provides a passthrough storage system that encrypts data at rest.
|
||||
type LocalStorage struct {
|
||||
root string
|
||||
key []byte
|
||||
filePerm fs.FileMode
|
||||
dirPerm fs.FileMode
|
||||
}
|
||||
|
||||
// NewLocalStorage creates a new LocalStorage.
|
||||
func NewLocalStorage(root string, key []byte) *LocalStorage {
|
||||
return &LocalStorage{
|
||||
root: root,
|
||||
key: key,
|
||||
filePerm: 0644,
|
||||
dirPerm: 0755,
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads and decrypts the data for the given key.
|
||||
func (s *LocalStorage) Read(key string) ([]byte, error) {
|
||||
path := filepath.Join(s.root, key)
|
||||
ciphertext, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chachapoly.Decrypt(ciphertext, s.key)
|
||||
}
|
||||
|
||||
// Write encrypts and writes the data for the given key.
|
||||
func (s *LocalStorage) Write(key string, data []byte) error {
|
||||
ciphertext, err := chachapoly.Encrypt(data, s.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(s.root, key)
|
||||
if err := os.MkdirAll(filepath.Dir(path), s.dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, ciphertext, s.filePerm)
|
||||
}
|
||||
|
||||
// Delete deletes the data for the given key.
|
||||
func (s *LocalStorage) Delete(key string) error {
|
||||
path := filepath.Join(s.root, key)
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// List lists the keys in the storage.
|
||||
func (s *LocalStorage) List(prefix string) ([]fs.FileInfo, error) {
|
||||
var files []fs.FileInfo
|
||||
err := filepath.Walk(filepath.Join(s.root, prefix), func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files = append(files, info)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package rootfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLocalStorage(t *testing.T) {
|
||||
// Create a temporary directory for testing.
|
||||
tempDir, err := os.MkdirTemp("", "enchantrix-test")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a new LocalStorage instance.
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = 1
|
||||
}
|
||||
storage := NewLocalStorage(tempDir, key)
|
||||
|
||||
// Test Write and Read.
|
||||
err = storage.Write("test.txt", []byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
data, err := storage.Read("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("hello"), data)
|
||||
|
||||
// Test List.
|
||||
files, err := storage.List("")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, files, 1)
|
||||
assert.Equal(t, "test.txt", files[0].Name())
|
||||
|
||||
// Test Delete.
|
||||
err = storage.Delete("test.txt")
|
||||
assert.NoError(t, err)
|
||||
_, err = os.Stat(filepath.Join(tempDir, "test.txt"))
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package rootfs
|
||||
|
||||
import "io/fs"
|
||||
|
||||
// Storage defines the interface for a passthrough storage system.
|
||||
type Storage interface {
|
||||
// Read reads the data for the given key.
|
||||
Read(key string) ([]byte, error)
|
||||
// Write writes the data for the given key.
|
||||
Write(key string, data []byte) error
|
||||
// Delete deletes the data for the given key.
|
||||
Delete(key string) error
|
||||
// List lists the keys in the storage.
|
||||
List(prefix string) ([]fs.FileInfo, error)
|
||||
}
|
||||
113
trix/trix.go
113
trix/trix.go
|
|
@ -1,113 +0,0 @@
|
|||
package trix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
MagicNumber = "TRIX"
|
||||
Version = 2
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
|
||||
ErrInvalidVersion = errors.New("trix: invalid version")
|
||||
)
|
||||
|
||||
// Trix represents the structure of a .trix file.
|
||||
type Trix struct {
|
||||
Header map[string]interface{}
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// Encode serializes a Trix struct into the .trix binary format.
|
||||
func Encode(trix *Trix) ([]byte, error) {
|
||||
headerBytes, err := json.Marshal(trix.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headerLength := uint32(len(headerBytes))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// Write Magic Number
|
||||
if _, err := buf.WriteString(MagicNumber); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write Version
|
||||
if err := buf.WriteByte(byte(Version)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write Header Length
|
||||
if err := binary.Write(buf, binary.BigEndian, headerLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write JSON Header
|
||||
if _, err := buf.Write(headerBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write Payload
|
||||
if _, err := buf.Write(trix.Payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Decode deserializes the .trix binary format into a Trix struct.
|
||||
func Decode(data []byte) (*Trix, error) {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
// Read and Verify Magic Number
|
||||
magic := make([]byte, 4)
|
||||
if _, err := io.ReadFull(buf, magic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if string(magic) != MagicNumber {
|
||||
return nil, ErrInvalidMagicNumber
|
||||
}
|
||||
|
||||
// Read and Verify Version
|
||||
version, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version != Version {
|
||||
return nil, ErrInvalidVersion
|
||||
}
|
||||
|
||||
// Read Header Length
|
||||
var headerLength uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &headerLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read JSON Header
|
||||
headerBytes := make([]byte, headerLength)
|
||||
if _, err := io.ReadFull(buf, headerBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var header map[string]interface{}
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read Payload
|
||||
payload, err := io.ReadAll(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Trix{
|
||||
Header: header,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package trix
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncodeDecode(t *testing.T) {
|
||||
header := map[string]interface{}{
|
||||
"content_type": "application/octet-stream",
|
||||
"encryption_algorithm": "chacha20poly1035",
|
||||
"nonce": "AAECAwQFBgcICQoLDA0ODxAREhMUFRY=",
|
||||
"created_at": "2025-10-30T12:00:00Z",
|
||||
}
|
||||
payload := []byte("This is a secret message.")
|
||||
|
||||
trix := &Trix{
|
||||
Header: header,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
encoded, err := Encode(trix)
|
||||
assert.NoError(t, err)
|
||||
|
||||
decoded, err := Decode(encoded)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(trix.Header, decoded.Header))
|
||||
assert.Equal(t, trix.Payload, decoded.Payload)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue