feat(crypha): scaffold steganography multi-tool with payload engine (M0-M1)

New beginner-tier Go project (crypha): a multi-format steganography tool that hides an encrypted payload across five carriers (image, audio, QR, zero-width text, PDF) from a cobra CLI and a planned bubbletea TUI.

M0 scaffold: cobra skeleton, config constants, justfile, golangci config. M1 engine spine: internal/bitio (MSB-first bit reader/writer), internal/payload (versioned AEAD envelope: Argon2id KDF, ChaCha20-Poly1305 default / AES-256-GCM alternate, flate compression, CRC32 integrity, header-as-AAD, hostile-parameter hardening), internal/carrier (Carrier interface + self-registering registry).

Verified: go build, go vet, gofmt, golangci-lint (0 issues), go test -race all green; coverage bitio 100% / carrier 91% / payload 88%.
This commit is contained in:
CarterPerez-dev 2026-07-14 17:03:34 -04:00
parent 816dd4506c
commit b89b57347d
17 changed files with 1061 additions and 0 deletions

View File

@ -0,0 +1,17 @@
# build output
/dist/
/crypha
# test / coverage
coverage.out
*.test
*.prof
# local dev docs (research / plans / context stay local)
/docs/
# editor / os
.idea/
.vscode/
*.swp
.DS_Store

View File

@ -0,0 +1,32 @@
# ©AngelaMos | 2026
# .golangci.yml
version: "2"
run:
timeout: 5m
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- misspell
- unconvert
- bodyclose
- gosec
settings:
gosec:
excludes:
- G115
formatters:
enable:
- gofmt
- goimports
issues:
max-issues-per-linter: 0
max-same-issues: 0

View File

@ -0,0 +1,14 @@
/*
©AngelaMos | 2026
main.go
Entry point for the crypha steganography multi-tool
*/
package main
import "github.com/CarterPerez-dev/crypha/internal/cli"
func main() {
cli.Execute()
}

View File

@ -0,0 +1,14 @@
module github.com/CarterPerez-dev/crypha
go 1.25.0
require (
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.52.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.47.0 // indirect
)

View File

@ -0,0 +1,14 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -0,0 +1,69 @@
/*
©AngelaMos | 2026
bitio.go
MSB-first bit reader and writer for carriers that embed data bit by bit
*/
package bitio
import "io"
type Reader struct {
in []byte
pos int
}
func NewReader(b []byte) *Reader {
return &Reader{in: b}
}
func (r *Reader) ReadBit() (byte, error) {
if r.pos >= len(r.in)*8 {
return 0, io.EOF
}
byteIdx := r.pos / 8
bitIdx := 7 - (r.pos % 8)
bit := (r.in[byteIdx] >> bitIdx) & 1
r.pos++
return bit, nil
}
func (r *Reader) TotalBits() int {
return len(r.in) * 8
}
func (r *Reader) Remaining() int {
return len(r.in)*8 - r.pos
}
type Writer struct {
out []byte
cur byte
fill int
}
func NewWriter() *Writer {
return &Writer{}
}
func (w *Writer) WriteBit(bit byte) {
w.cur = (w.cur << 1) | (bit & 1)
w.fill++
if w.fill == 8 {
w.out = append(w.out, w.cur)
w.cur = 0
w.fill = 0
}
}
func (w *Writer) Bytes() []byte {
if w.fill == 0 {
return w.out
}
return append(w.out, w.cur<<(8-w.fill))
}
func (w *Writer) BitsWritten() int {
return len(w.out)*8 + w.fill
}

View File

@ -0,0 +1,82 @@
/*
©AngelaMos | 2026
bitio_test.go
Round-trip and boundary tests for the MSB-first bit reader and writer
*/
package bitio
import (
"bytes"
"io"
"testing"
)
func TestRoundTripByteAligned(t *testing.T) {
cases := [][]byte{
{},
{0x00},
{0xFF},
{0xA5, 0x5A, 0x00, 0xFF, 0x0F, 0xF0},
[]byte("crypha carries the secret"),
}
for _, in := range cases {
r := NewReader(in)
w := NewWriter()
for {
bit, err := r.ReadBit()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("ReadBit: %v", err)
}
w.WriteBit(bit)
}
if got := w.Bytes(); !bytes.Equal(got, in) {
t.Errorf("round-trip mismatch: got %x want %x", got, in)
}
}
}
func TestMSBFirstOrder(t *testing.T) {
r := NewReader([]byte{0b10000001})
want := []byte{1, 0, 0, 0, 0, 0, 0, 1}
for i, wbit := range want {
got, err := r.ReadBit()
if err != nil {
t.Fatalf("bit %d: %v", i, err)
}
if got != wbit {
t.Errorf("bit %d: got %d want %d", i, got, wbit)
}
}
}
func TestReaderCounters(t *testing.T) {
r := NewReader([]byte{0x00, 0x00})
if r.TotalBits() != 16 {
t.Fatalf("TotalBits: got %d want 16", r.TotalBits())
}
if _, err := r.ReadBit(); err != nil {
t.Fatal(err)
}
if r.Remaining() != 15 {
t.Errorf("Remaining: got %d want 15", r.Remaining())
}
}
func TestWriterPadsPartialByte(t *testing.T) {
w := NewWriter()
w.WriteBit(1)
w.WriteBit(1)
w.WriteBit(1)
if w.BitsWritten() != 3 {
t.Fatalf("BitsWritten: got %d want 3", w.BitsWritten())
}
got := w.Bytes()
if len(got) != 1 || got[0] != 0b11100000 {
t.Errorf("partial byte: got %08b want 11100000", got[0])
}
}

View File

@ -0,0 +1,67 @@
/*
©AngelaMos | 2026
carrier.go
The Carrier interface and self-registering registry that every format plugs into
*/
package carrier
import (
"io"
"sort"
)
type Carrier interface {
Format() string
Hide(cover io.Reader, payload []byte, out io.Writer) error
Reveal(stego io.Reader) ([]byte, error)
Capacity(cover io.Reader) (int, error)
Sniff(stego io.ReadSeeker) bool
}
var registry = map[string]Carrier{}
func Register(c Carrier) {
registry[c.Format()] = c
}
func Get(name string) (Carrier, bool) {
c, ok := registry[name]
return c, ok
}
func All() []Carrier {
out := make([]Carrier, 0, len(registry))
for _, c := range registry {
out = append(out, c)
}
sort.Slice(out, func(i, j int) bool {
return out[i].Format() < out[j].Format()
})
return out
}
func Formats() []string {
out := make([]string, 0, len(registry))
for name := range registry {
out = append(out, name)
}
sort.Strings(out)
return out
}
func Detect(stego io.ReadSeeker) (Carrier, bool) {
for _, c := range All() {
if _, err := stego.Seek(0, io.SeekStart); err != nil {
return nil, false
}
if c.Sniff(stego) {
if _, err := stego.Seek(0, io.SeekStart); err != nil {
return nil, false
}
return c, true
}
}
return nil, false
}

View File

@ -0,0 +1,73 @@
/*
©AngelaMos | 2026
carrier_test.go
Registry behaviour tests using fake carriers
*/
package carrier
import (
"bytes"
"io"
"testing"
)
type fakeCarrier struct {
format string
magic byte
}
func (f fakeCarrier) Format() string { return f.format }
func (f fakeCarrier) Hide(_ io.Reader, _ []byte, _ io.Writer) error { return nil }
func (f fakeCarrier) Reveal(_ io.Reader) ([]byte, error) { return nil, nil }
func (f fakeCarrier) Capacity(_ io.Reader) (int, error) { return 0, nil }
func (f fakeCarrier) Sniff(stego io.ReadSeeker) bool {
head := make([]byte, 1)
if _, err := io.ReadFull(stego, head); err != nil {
return false
}
return head[0] == f.magic
}
func TestRegisterGetFormats(t *testing.T) {
registry = map[string]Carrier{}
Register(fakeCarrier{format: "zeta", magic: 0x01})
Register(fakeCarrier{format: "alpha", magic: 0x02})
if _, ok := Get("alpha"); !ok {
t.Fatal("expected alpha to be registered")
}
if _, ok := Get("missing"); ok {
t.Fatal("did not expect missing to resolve")
}
got := Formats()
if len(got) != 2 || got[0] != "alpha" || got[1] != "zeta" {
t.Errorf("Formats not sorted: %v", got)
}
if all := All(); len(all) != 2 || all[0].Format() != "alpha" {
t.Errorf("All not sorted: %v", all)
}
}
func TestDetect(t *testing.T) {
registry = map[string]Carrier{}
Register(fakeCarrier{format: "alpha", magic: 0x02})
Register(fakeCarrier{format: "beta", magic: 0x03})
stego := bytes.NewReader([]byte{0x03, 0xFF, 0xEE})
c, ok := Detect(stego)
if !ok || c.Format() != "beta" {
t.Fatalf("Detect: got %v ok=%v want beta", c, ok)
}
none := bytes.NewReader([]byte{0x99})
if _, ok := Detect(none); ok {
t.Error("expected no match for unknown magic")
}
}

View File

@ -0,0 +1,34 @@
/*
©AngelaMos | 2026
root.go
Cobra root command and CLI entry wiring for crypha
*/
package cli
import (
"fmt"
"os"
"github.com/CarterPerez-dev/crypha/internal/config"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: config.BinaryName,
Short: config.ShortDescription,
Long: config.LongDescription,
Version: config.Version,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.SetVersionTemplate(config.BinaryName + " {{.Version}}\n")
}

View File

@ -0,0 +1,17 @@
/*
©AngelaMos | 2026
config.go
Central constants for crypha so no magic numbers or strings live elsewhere
*/
package config
const (
BinaryName = "crypha"
Version = "0.1.0"
ShortDescription = "Multi-format steganography for images, audio, QR, text, and PDFs"
LongDescription = "crypha hides an encrypted payload inside five carrier types " +
"(image, audio, QR, zero-width text, PDF) and extracts it back, " +
"from a scriptable CLI or an interactive TUI."
)

View File

@ -0,0 +1,35 @@
/*
©AngelaMos | 2026
compress.go
Optional flate compression applied before encryption in the payload envelope
*/
package payload
import (
"bytes"
"compress/flate"
"io"
)
func compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w, err := flate.NewWriter(&buf, flate.BestCompression)
if err != nil {
return nil, err
}
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func decompress(data []byte) ([]byte, error) {
r := flate.NewReader(bytes.NewReader(data))
defer func() { _ = r.Close() }()
return io.ReadAll(r)
}

View File

@ -0,0 +1,75 @@
/*
©AngelaMos | 2026
crypto.go
Argon2id key derivation and AEAD selection for the payload envelope
*/
package payload
import (
"crypto/aes"
"crypto/cipher"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
)
type kdfParams struct {
time uint32
memory uint32
threads uint8
}
func paramsForStrength(s Strength) kdfParams {
if s == StrengthHigh {
return kdfParams{time: argonHighTime, memory: argonHighMemory, threads: argonThreads}
}
return kdfParams{time: argonDefaultTime, memory: argonDefaultMemory, threads: argonThreads}
}
func (p kdfParams) valid() bool {
if p.threads == 0 || p.time == 0 {
return false
}
if p.memory > argonMaxMemory {
return false
}
return p.memory >= 8*uint32(p.threads)
}
func deriveKey(passphrase, salt []byte, p kdfParams) []byte {
return argon2.IDKey(passphrase, salt, p.time, p.memory, p.threads, keyLen)
}
func newAEAD(c Cipher, key []byte) (cipher.AEAD, byte, error) {
switch c {
case CipherAES256GCM:
aead, err := gcm(key)
return aead, cipherIDAES256GCM, err
case CipherChaCha20, "":
aead, err := chacha20poly1305.New(key)
return aead, cipherIDChaCha20, err
default:
return nil, 0, ErrUnknownCipher
}
}
func aeadByID(id byte, key []byte) (cipher.AEAD, error) {
switch id {
case cipherIDAES256GCM:
return gcm(key)
case cipherIDChaCha20:
return chacha20poly1305.New(key)
default:
return nil, ErrUnknownCipher
}
}
func gcm(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}

View File

@ -0,0 +1,202 @@
/*
©AngelaMos | 2026
envelope.go
Pack and unpack the crypha payload envelope
Layout: magic(4) ver(1) flags(1) [cipher(1) params(9) salt(16) nonce(12) if encrypted]
len(4) body(N) crc32(4). The header up to nonce is the AEAD additional data.
*/
package payload
import (
"bytes"
"crypto/rand"
"encoding/binary"
"hash/crc32"
)
func Pack(data []byte, opts Options) ([]byte, error) {
if len(data) == 0 {
return nil, ErrEmptyPayload
}
var flags byte
body := data
if opts.Compress {
c, err := compress(body)
if err != nil {
return nil, err
}
body = c
flags |= flagCompressed
}
header := new(bytes.Buffer)
header.Write(magic[:])
header.WriteByte(currentVersion)
if len(opts.Passphrase) > 0 {
flags |= flagEncrypted
header.WriteByte(flags)
salt := make([]byte, saltLen)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
params := paramsForStrength(opts.Strength)
key := deriveKey(opts.Passphrase, salt, params)
aead, cipherID, err := newAEAD(opts.Cipher, key)
if err != nil {
return nil, err
}
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
header.WriteByte(cipherID)
var pbuf [paramsLen]byte
binary.BigEndian.PutUint32(pbuf[0:4], params.time)
binary.BigEndian.PutUint32(pbuf[4:8], params.memory)
pbuf[8] = params.threads
header.Write(pbuf[:])
header.Write(salt)
header.Write(nonce)
body = aead.Seal(nil, nonce, body, header.Bytes())
} else {
header.WriteByte(flags)
}
out := new(bytes.Buffer)
out.Write(header.Bytes())
var meta [lenField]byte
binary.BigEndian.PutUint32(meta[:], uint32(len(body)))
out.Write(meta[:])
out.Write(body)
binary.BigEndian.PutUint32(meta[:], crc32.ChecksumIEEE(body))
out.Write(meta[:])
return out.Bytes(), nil
}
type parsed struct {
flags byte
aad []byte
cipherID byte
params kdfParams
salt []byte
nonce []byte
body []byte
}
func parse(env []byte) (*parsed, error) {
off := 0
remaining := func(n int) bool { return off+n <= len(env) }
if !remaining(len(magic)) || !bytes.Equal(env[0:len(magic)], magic[:]) {
return nil, ErrBadMagic
}
off = len(magic)
if !remaining(1) {
return nil, ErrTruncated
}
if env[off] != currentVersion {
return nil, ErrUnsupportedVersion
}
off++
if !remaining(1) {
return nil, ErrTruncated
}
p := &parsed{flags: env[off]}
off++
if p.flags&flagEncrypted != 0 {
if !remaining(1 + paramsLen + saltLen + nonceLen) {
return nil, ErrTruncated
}
p.cipherID = env[off]
off++
p.params = kdfParams{
time: binary.BigEndian.Uint32(env[off : off+4]),
memory: binary.BigEndian.Uint32(env[off+4 : off+8]),
threads: env[off+8],
}
off += paramsLen
if !p.params.valid() {
return nil, ErrBadParams
}
p.salt = env[off : off+saltLen]
off += saltLen
p.nonce = env[off : off+nonceLen]
off += nonceLen
p.aad = env[0:off]
}
if !remaining(lenField) {
return nil, ErrTruncated
}
bodyLen := int(binary.BigEndian.Uint32(env[off : off+lenField]))
off += lenField
if bodyLen < 0 || !remaining(bodyLen) {
return nil, ErrTruncated
}
p.body = env[off : off+bodyLen]
off += bodyLen
if !remaining(crcField) {
return nil, ErrTruncated
}
if crc32.ChecksumIEEE(p.body) != binary.BigEndian.Uint32(env[off:off+crcField]) {
return nil, ErrChecksumMismatch
}
return p, nil
}
func Unpack(env, passphrase []byte) ([]byte, error) {
p, err := parse(env)
if err != nil {
return nil, err
}
body := p.body
if p.flags&flagEncrypted != 0 {
if len(passphrase) == 0 {
return nil, ErrPassphraseRequired
}
key := deriveKey(passphrase, p.salt, p.params)
aead, err := aeadByID(p.cipherID, key)
if err != nil {
return nil, err
}
plain, err := aead.Open(nil, p.nonce, body, p.aad)
if err != nil {
return nil, ErrDecrypt
}
body = plain
}
if p.flags&flagCompressed != 0 {
return decompress(body)
}
return body, nil
}
func Validate(env []byte) error {
_, err := parse(env)
return err
}
func IsEncrypted(env []byte) (bool, error) {
p, err := parse(env)
if err != nil {
return false, err
}
return p.flags&flagEncrypted != 0, nil
}

View File

@ -0,0 +1,158 @@
/*
©AngelaMos | 2026
envelope_test.go
Round-trip, integrity, and hardening tests for the payload envelope
*/
package payload
import (
"bytes"
"errors"
"testing"
)
var secret = []byte("the eagle lands at midnight -- coordinates 41.40N 2.17E")
func TestRoundTrip(t *testing.T) {
pass := []byte("correct horse battery staple")
cases := []struct {
name string
opts Options
}{
{"plain", Options{}},
{"compressed", Options{Compress: true}},
{"encrypted-chacha", Options{Passphrase: pass}},
{"encrypted-aes", Options{Passphrase: pass, Cipher: CipherAES256GCM}},
{"encrypted-compressed", Options{Passphrase: pass, Compress: true}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
env, err := Pack(secret, tc.opts)
if err != nil {
t.Fatalf("Pack: %v", err)
}
got, err := Unpack(env, tc.opts.Passphrase)
if err != nil {
t.Fatalf("Unpack: %v", err)
}
if !bytes.Equal(got, secret) {
t.Errorf("round-trip mismatch: got %q", got)
}
})
}
}
func TestWrongPassphraseFails(t *testing.T) {
env, err := Pack(secret, Options{Passphrase: []byte("right")})
if err != nil {
t.Fatal(err)
}
if _, err := Unpack(env, []byte("wrong")); !errors.Is(err, ErrDecrypt) {
t.Errorf("got %v want ErrDecrypt", err)
}
}
func TestMissingPassphraseFails(t *testing.T) {
env, err := Pack(secret, Options{Passphrase: []byte("right")})
if err != nil {
t.Fatal(err)
}
if _, err := Unpack(env, nil); !errors.Is(err, ErrPassphraseRequired) {
t.Errorf("got %v want ErrPassphraseRequired", err)
}
}
func TestTamperDetected(t *testing.T) {
env, err := Pack(secret, Options{Passphrase: []byte("right")})
if err != nil {
t.Fatal(err)
}
env[len(env)-6] ^= 0xFF // flip a byte inside the ciphertext body
if _, err := Unpack(env, []byte("right")); err == nil {
t.Error("expected an error on tampered envelope")
}
}
func TestChecksumMismatch(t *testing.T) {
env, err := Pack(secret, Options{})
if err != nil {
t.Fatal(err)
}
env[len(env)-5] ^= 0xFF // flip a body byte on the plaintext path
if _, err := Unpack(env, nil); !errors.Is(err, ErrChecksumMismatch) {
t.Errorf("got %v want ErrChecksumMismatch", err)
}
}
func TestBadMagic(t *testing.T) {
env, _ := Pack(secret, Options{})
env[0] ^= 0xFF
if _, err := Unpack(env, nil); !errors.Is(err, ErrBadMagic) {
t.Errorf("got %v want ErrBadMagic", err)
}
}
func TestVersionReject(t *testing.T) {
env, _ := Pack(secret, Options{})
env[len(magic)] = 0x7F
if _, err := Unpack(env, nil); !errors.Is(err, ErrUnsupportedVersion) {
t.Errorf("got %v want ErrUnsupportedVersion", err)
}
}
func TestTruncated(t *testing.T) {
env, _ := Pack(secret, Options{})
if _, err := Unpack(env[:5], nil); !errors.Is(err, ErrTruncated) {
t.Errorf("got %v want ErrTruncated", err)
}
}
func TestEmptyPayload(t *testing.T) {
if _, err := Pack(nil, Options{}); !errors.Is(err, ErrEmptyPayload) {
t.Errorf("got %v want ErrEmptyPayload", err)
}
}
func TestHostileParamsRejected(t *testing.T) {
env, err := Pack(secret, Options{Passphrase: []byte("right")})
if err != nil {
t.Fatal(err)
}
// zero out the Argon2 time field (offset magic+ver+flags+cipherID)
timeOff := len(magic) + 1 + 1 + 1
for i := 0; i < 4; i++ {
env[timeOff+i] = 0
}
if _, err := Unpack(env, []byte("right")); !errors.Is(err, ErrBadParams) {
t.Errorf("got %v want ErrBadParams", err)
}
}
func TestValidateAndIsEncrypted(t *testing.T) {
plain, _ := Pack(secret, Options{})
enc, _ := Pack(secret, Options{Passphrase: []byte("k")})
if err := Validate(plain); err != nil {
t.Errorf("Validate(plain): %v", err)
}
if err := Validate([]byte("garbage")); err == nil {
t.Error("Validate(garbage) should fail")
}
if e, _ := IsEncrypted(plain); e {
t.Error("plain reported encrypted")
}
if e, _ := IsEncrypted(enc); !e {
t.Error("encrypted reported plain")
}
}
func TestStrengthParams(t *testing.T) {
if p := paramsForStrength(StrengthHigh); p.memory != argonHighMemory || p.time != argonHighTime {
t.Errorf("high params wrong: %+v", p)
}
if p := paramsForStrength(StrengthDefault); p.memory != argonDefaultMemory {
t.Errorf("default params wrong: %+v", p)
}
}

View File

@ -0,0 +1,69 @@
/*
©AngelaMos | 2026
payload.go
Envelope types, constants, and errors shared across the crypha payload codec
*/
package payload
import "errors"
type Cipher string
const (
CipherChaCha20 Cipher = "chacha20"
CipherAES256GCM Cipher = "aes256gcm"
)
type Strength string
const (
StrengthDefault Strength = "default"
StrengthHigh Strength = "high"
)
type Options struct {
Passphrase []byte
Compress bool
Cipher Cipher
Strength Strength
}
const (
currentVersion byte = 0x01
flagEncrypted byte = 1 << 0
flagCompressed byte = 1 << 1
cipherIDChaCha20 byte = 0x00
cipherIDAES256GCM byte = 0x01
saltLen = 16
nonceLen = 12
keyLen = 32
lenField = 4
crcField = 4
paramsLen = 9
argonDefaultTime uint32 = 3
argonDefaultMemory uint32 = 64 * 1024
argonHighTime uint32 = 1
argonHighMemory uint32 = 2048 * 1024
argonMaxMemory uint32 = argonHighMemory
argonThreads uint8 = 4
)
var magic = [4]byte{0xC7, 0x1A, 0x9E, 0x5B}
var (
ErrEmptyPayload = errors.New("crypha: empty payload")
ErrBadMagic = errors.New("crypha: not a crypha payload (bad magic)")
ErrUnsupportedVersion = errors.New("crypha: unsupported envelope version")
ErrTruncated = errors.New("crypha: truncated envelope")
ErrBadParams = errors.New("crypha: invalid key-derivation parameters")
ErrChecksumMismatch = errors.New("crypha: checksum mismatch")
ErrPassphraseRequired = errors.New("crypha: payload is encrypted, passphrase required")
ErrDecrypt = errors.New("crypha: decryption failed (wrong passphrase or tampered data)")
ErrUnknownCipher = errors.New("crypha: unknown cipher")
)

View File

@ -0,0 +1,89 @@
# =============================================================================
# ©AngelaMos | 2026
# justfile - crypha steganography multi-tool
# =============================================================================
set shell := ["bash", "-uc"]
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
binary := "crypha"
version := `git describe --tags --always 2>/dev/null || echo "dev"`
# =============================================================================
# Default
# =============================================================================
default:
@just --list --unsorted
# =============================================================================
# Formatting and Linting
# =============================================================================
[group('lint')]
fmt:
gofmt -w -s cmd/ internal/
[group('lint')]
vet:
go vet ./...
[group('lint')]
lint:
GOTOOLCHAIN=go1.25.7 golangci-lint run ./...
# =============================================================================
# Testing
# =============================================================================
[group('test')]
test *ARGS:
go test ./... {{ARGS}}
[group('test')]
test-race:
go test -race ./...
[group('test')]
cov:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
[group('test')]
cov-html: cov
go tool cover -html=coverage.out
# =============================================================================
# Build and Run
# =============================================================================
[group('build')]
build:
go build -o dist/{{binary}} ./cmd/{{binary}}
[group('build')]
run *ARGS:
go run ./cmd/{{binary}} {{ARGS}}
# =============================================================================
# CI / Housekeeping
# =============================================================================
[group('ci')]
ci: fmt vet test build
[group('ci')]
tidy:
go mod tidy
[group('ci')]
info:
@echo "Binary: {{binary}}"
@echo "Version: {{version}}"
@echo "Go: $(go version)"
[group('ci')]
clean:
-rm -rf dist
-rm -f coverage.out
@echo "Cleaned"