wrapper

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 20, 2026 License: MIT Imports: 8 Imported by: 0

README

ITB Examples Code

Companion code for the ITB Quick Start. Every example mirrors one configuration from the ITB README and adds a thin user-side outer cipher envelope so the on-wire bytes look like generic stream cipher output rather than ITB format pixel containers + per-chunk prefix.

Threat model

ITB encrypts content into RGBWYOPA pixel containers. The construction provides content-deniability unconditionally — no plaintext bit can be extracted from the wire. The wire pattern itself, however, is parseable by an observer who knows the ITB format:

  • Non-AEAD path: per-chunk header carries width / height / container layout.
  • Streaming AEAD path: a once per-stream 32-byte streamID prefix plus per-chunk nonce || W || H || container || flag_byte.

A passive observer who knows ITB ships with an 8-channel pixel container and a 32-byte streamID prefix can pattern-match the bytes. The format-deniability wrap hides that surface under a generic outer cipher: AES-128-CTR, ChaCha20 (RFC8439), or SipHash-2-4 in CTR mode. After wrapping, the wire is nonce || keystream-XOR(bytestream) — the same shape used by countless other protocols. An observer sees a small leading nonce followed by pseudorandom-looking bytes; pattern-matching does not distinguish ITB from any other stream cipher payload.

This is not a random-oracle indistinguishability claim. It is a "looks like a different well-known cipher" claim. The wrap exists for format-deniability ONLY; ITB already provides confidentiality (content-deniability) and the AEAD path already provides per-stream and per-chunk integrity. The Non-AEAD streaming path has no integrity by design and the wrap does not add any.

Wrapper API

The wrapper package exposes one Keystream interface satisfied by all three outer ciphers, plus two wrap-shape helpers:

Helper Wire format Use case
Wrap / Unwrap nonce + keystream-XOR(blob) Single Message Encrypt / EncryptAuth output
NewWrapWriter / NewUnwrapReader nonce + keystream-XOR(continuous bytestream) streaming use — IO-Driven, or User-Driven Loop where caller-side framing (e.g. per-chunk u32_LE length prefixes) is written through the wrap-writer so the framing bytes also pass through the keystream XOR

The single keystream advances monotonically across all bytes within one wrap session. A fresh CSPRNG nonce is generated per session; emitted once at stream start; never reused across sessions. This is standard CTR mode usage — within one stream, one nonce + counter is correct.

No length-prefix or other framing byte appears in cleartext on the wire in any wrap shape. The User-Driven Loop emits length prefixes through the wrap-writer so they get XORed into the keystream alongside the chunk bodies.

Outer ciphers

Cipher Key Nonce Notes
AES-128-CTR 16 B 16 B stdlib crypto/aes + crypto/cipher.NewCTR. AES-NI accelerated.
ChaCha20 (RFC 8439) 32 B 12 B golang.org/x/crypto/chacha20. No AES-NI dependency.
SipHash-2-4 in CTR mode 16 B 16 B github.com/dchest/siphash PRF. Custom CTR construction; sound under standard PRF assumption.

The SipHash-CTR construction:

  • 16-byte SipHash key = wrapper key.
  • 16-byte nonce split into (nonce_hi, nonce_lo) 64-bit halves.
  • Each keystream block: siphash.Hash(key, nonce_hi || (nonce_lo XOR counter_LE)) — 8-byte output, XORed with plaintext.
  • Counter increments per block; nonce stays fixed for the stream.

Quick Start

Code paths under tools/eitb/main.go. Run the matrix:

go run ./tools/eitb       # run every example × every cipher
go run ./tools/eitb -help # print help
1. Streaming AEAD Easy (MAC Authenticated, IO-Driven)

ITB Call: easy.Encryptor.EncryptStreamAuthIO / DecryptStreamAuthIO. Wrap shape: NewWrapWriter / NewUnwrapReader over the continuous bytestream ITB emits.

enc := easy.New("areion512", 1024, "hmac-blake3")
defer enc.Close()
enc.SetNonceBits(512); enc.SetBarrierFill(4); enc.SetBitSoup(1); enc.SetLockSoup(1)

outerKey, _ := wrapper.GenerateKey(cipherName)

// Sender
var wireBuf bytes.Buffer
wrapWriter, _ := wrapper.NewWrapWriter(cipherName, outerKey, &wireBuf)
_ = enc.EncryptStreamAuthIO(plaintextReader, wrapWriter, chunkSize)

// Receiver
unwrapReader, _ := wrapper.NewUnwrapReader(cipherName, outerKey, bytes.NewReader(wireBuf.Bytes()))
var dst bytes.Buffer
_ = enc.DecryptStreamAuthIO(unwrapReader, &dst)
2. Streaming AEAD Low-Level (MAC Authenticated, IO-Driven)

ITB Call: itb.EncryptStreamAuth / itb.DecryptStreamAuth with three explicit *Seed512 handles plus macs.Make("hmac-blake3", key). Wrap shape: NewWrapWriter / NewUnwrapReader.

hashFn, _, _ := hashes.Make512("areion512")
noise, _ := itb.NewSeed512(1024, hashFn)
data,  _ := itb.NewSeed512(1024, hashFn)
start, _ := itb.NewSeed512(1024, hashFn)

macKey := make([]byte, 32); rand.Read(macKey)
macFunc, _ := macs.Make("hmac-blake3", macKey)

outerKey, _ := wrapper.GenerateKey(cipherName)
wrapWriter, _ := wrapper.NewWrapWriter(cipherName, outerKey, &wireBuf)
_ = itb.EncryptStreamAuth(noise, data, start, plaintextReader, wrapWriter, macFunc, chunkSize)

// receiver
unwrapReader, _ := wrapper.NewUnwrapReader(cipherName, outerKey, bytes.NewReader(wireBuf.Bytes()))
_ = itb.DecryptStreamAuth(noise, data, start, unwrapReader, &dst, macFunc)
3. Streaming Easy (No MAC, IO-Driven)

ITB Call: easy.Encryptor.EncryptStreamIO / DecryptStreamIO. Wrap shape: NewWrapWriter / NewUnwrapReader. The outer cipher contributes format-deniability only — does not retro-fit integrity onto the No MAC ITB path.

enc := easy.New("areion512", 1024)
// Set* configuration unchanged from authenticated variant.
wrapWriter, _ := wrapper.NewWrapWriter(cipherName, outerKey, &wireBuf)
_ = enc.EncryptStreamIO(plaintextReader, wrapWriter, chunkSize)

unwrapReader, _ := wrapper.NewUnwrapReader(cipherName, outerKey, bytes.NewReader(wireBuf.Bytes()))
_ = enc.DecryptStreamIO(unwrapReader, &dst)
4. Streaming Easy (No MAC, User-Driven Loop)

The README's "Alternative — User-Driven Loop" pattern: each chunk is one independent enc.Encrypt(buf[:n]) call. Wrap shape: NewWrapWriter / NewUnwrapReader driven by a caller loop that emits u32_LE_len || ct per chunk through the wrapped writer. Length prefix and chunk body both pass through the keystream XOR — no length appears in cleartext on the wire.

outerKey, _ := wrapper.GenerateKey(cipherName)

// Sender
var wireBuf bytes.Buffer
wrapWriter, _ := wrapper.NewWrapWriter(cipherName, outerKey, &wireBuf)

buf := make([]byte, chunkSize)
for {
    n, rerr := io.ReadFull(plaintextReader, buf)
    if rerr == io.EOF { break }
    ct, _ := enc.Encrypt(buf[:n])
    _ = binary.Write(wrapWriter, binary.LittleEndian, uint32(len(ct)))
    _, _ = wrapWriter.Write(ct)
    if rerr == io.ErrUnexpectedEOF { break }
}

// Receiver — read u32_LE length then body through the unwrap-reader, looping until EOF.
unwrapReader, _ := wrapper.NewUnwrapReader(cipherName, outerKey, bytes.NewReader(wireBuf.Bytes()))
for {
    var ctLen uint32
    if err := binary.Read(unwrapReader, binary.LittleEndian, &ctLen); err == io.EOF {
        break
    } else if err != nil {
        panic(err)
    }
    ctBuf := make([]byte, ctLen)
    _, _ = io.ReadFull(unwrapReader, ctBuf)
    pt, _ := enc.Decrypt(ctBuf)
    out.Write(pt)
}
5. Streaming Low-Level (No MAC, IO-Driven)

ITB Call: itb.EncryptStream / itb.DecryptStream. Wrap shape: NewWrapWriter / NewUnwrapReader.

hashFn, _, _ := hashes.Make512("areion512")
noise, _ := itb.NewSeed512(1024, hashFn)
data,  _ := itb.NewSeed512(1024, hashFn)
start, _ := itb.NewSeed512(1024, hashFn)

wrapWriter, _ := wrapper.NewWrapWriter(cipherName, outerKey, &wireBuf)
_ = itb.EncryptStream(noise, data, start, plaintextReader, wrapWriter, chunkSize)

unwrapReader, _ := wrapper.NewUnwrapReader(cipherName, outerKey, bytes.NewReader(wireBuf.Bytes()))
_ = itb.DecryptStream(noise, data, start, unwrapReader, &dst)
6. Streaming Low-Level (No MAC, User-Driven Loop)

Per-chunk itb.Encrypt / itb.Decrypt with caller-side framing. Wrap shape: NewWrapWriter / NewUnwrapReader. Each chunk is emitted as u32_LE_len || ct through the wrap-writer; the length and the body both pass through the keystream XOR.

outerKey, _ := wrapper.GenerateKey(cipherName)

var wireBuf bytes.Buffer
wrapWriter, _ := wrapper.NewWrapWriter(cipherName, outerKey, &wireBuf)

buf := make([]byte, chunkSize)
for {
    n, rerr := io.ReadFull(plaintextReader, buf)
    if rerr == io.EOF { break }
    ct, _ := itb.Encrypt(noise, data, start, buf[:n])
    _ = binary.Write(wrapWriter, binary.LittleEndian, uint32(len(ct)))
    _, _ = wrapWriter.Write(ct)
    if rerr == io.ErrUnexpectedEOF { break }
}

// Receiver
unwrapReader, _ := wrapper.NewUnwrapReader(cipherName, outerKey, bytes.NewReader(wireBuf.Bytes()))
for {
    var ctLen uint32
    if err := binary.Read(unwrapReader, binary.LittleEndian, &ctLen); err == io.EOF {
        break
    } else if err != nil {
        panic(err)
    }
    ctBuf := make([]byte, ctLen)
    _, _ = io.ReadFull(unwrapReader, ctBuf)
    pt, _ := itb.Decrypt(noise, data, start, ctBuf)
    out.Write(pt)
}
7. Easy: Areion-SoEM-512 (No MAC, Single Message)

ITB Call: enc.Encrypt(plaintext) returns one ITB blob. Wrap shape: Wrapnonce || ks-XOR(blob). Wire shape mirrors any "outer cipher with a fresh nonce and an opaque payload" pattern.

enc := easy.New("areion512", 2048)
defer enc.Close()
enc.SetNonceBits(512); enc.SetBarrierFill(4); enc.SetBitSoup(1); enc.SetLockSoup(1)

encrypted, _ := enc.Encrypt(plaintext)

outerKey, _ := wrapper.GenerateKey(cipherName)
wire, _ := wrapper.Wrap(cipherName, outerKey, encrypted)

// receiver
recovered, _ := wrapper.Unwrap(cipherName, outerKey, wire)
pt, _ := enc.Decrypt(recovered)
8. Easy: Areion-SoEM-512 + HMAC-BLAKE3 (MAC Authenticated, Single Message)

ITB Call: enc.EncryptAuth / enc.DecryptAuth. Wrap shape: Wrap. The ITB-internal 32-byte MAC tag remains inside the RGBWYOPA container; outer cipher is format-deniability only.

enc := easy.New("areion512", 2048, "hmac-blake3")
defer enc.Close()
enc.SetNonceBits(512); enc.SetBarrierFill(4); enc.SetBitSoup(1); enc.SetLockSoup(1)

encrypted, _ := enc.EncryptAuth(plaintext)

outerKey, _ := wrapper.GenerateKey(cipherName)
wire, _ := wrapper.Wrap(cipherName, outerKey, encrypted)

// receiver
recovered, _ := wrapper.Unwrap(cipherName, outerKey, wire)
pt, _ := enc.DecryptAuth(recovered)
9. Low-Level: Areion-SoEM-512 (No MAC, Single Message)

ITB Call: width-less itb.Encrypt(noise, data, start, plaintext) / itb.Decrypt(...) with three explicit *Seed512 handles built from hashes.Make512("areion512"). Wrap shape: Wrapnonce || ks-XOR(blob). Wire shape matches example 7; the difference is that the seed material is held by caller-side handles rather than by an easy.Encryptor instance.

itb.SetNonceBits(512); itb.SetBarrierFill(4); itb.SetBitSoup(1); itb.SetLockSoup(1)

hashFn, _, _ := hashes.Make512("areion512")
noise, _ := itb.NewSeed512(2048, hashFn)
data,  _ := itb.NewSeed512(2048, hashFn)
start, _ := itb.NewSeed512(2048, hashFn)

encrypted, _ := itb.Encrypt(noise, data, start, plaintext)

outerKey, _ := wrapper.GenerateKey(cipherName)
wire, _ := wrapper.Wrap(cipherName, outerKey, encrypted)

// receiver
recovered, _ := wrapper.Unwrap(cipherName, outerKey, wire)
pt, _ := itb.Decrypt(noise, data, start, recovered)
10. Low-Level: Areion-SoEM-512 + HMAC-BLAKE3 (MAC Authenticated, Single Message)

ITB Call: width-less itb.EncryptAuth(noise, data, start, plaintext, macFunc) / itb.DecryptAuth(...) with the MAC closure constructed via macs.Make("hmac-blake3", macKey). Wrap shape: Wrap. The ITB-internal 32-byte MAC tag remains inside the RGBWYOPA container; outer cipher is format-deniability only.

itb.SetNonceBits(512); itb.SetBarrierFill(4); itb.SetBitSoup(1); itb.SetLockSoup(1)

hashFn, _, _ := hashes.Make512("areion512")
noise, _ := itb.NewSeed512(2048, hashFn)
data,  _ := itb.NewSeed512(2048, hashFn)
start, _ := itb.NewSeed512(2048, hashFn)

macKey := make([]byte, 32); rand.Read(macKey)
macFunc, _ := macs.Make("hmac-blake3", macKey)

encrypted, _ := itb.EncryptAuth(noise, data, start, plaintext, macFunc)

outerKey, _ := wrapper.GenerateKey(cipherName)
wire, _ := wrapper.Wrap(cipherName, outerKey, encrypted)

// receiver
recovered, _ := wrapper.Unwrap(cipherName, outerKey, wire)
pt, _ := itb.DecryptAuth(noise, data, start, recovered, macFunc)

Verification matrix

Every example × cipher combination round-trips against random plaintext (1 KiB for Single Message, 64 KiB for streaming) with sha256 byte-equality. Sample run:

[PASS] aead-easy-io                + aes        pt=65536 wire=90208
[PASS] aead-easy-io                + chacha     pt=65536 wire=90204
[PASS] aead-easy-io                + siphash    pt=65536 wire=90208
[PASS] aead-lowlevel-io            + aes        pt=65536 wire=90208
[PASS] aead-lowlevel-io            + chacha     pt=65536 wire=90204
[PASS] aead-lowlevel-io            + siphash    pt=65536 wire=90208
[PASS] noaead-easy-io              + aes        pt=65536 wire=90176
[PASS] noaead-easy-io              + chacha     pt=65536 wire=90172
[PASS] noaead-easy-io              + siphash    pt=65536 wire=90176
[PASS] noaead-easy-userloop        + aes        pt=65536 wire=90192
[PASS] noaead-easy-userloop        + chacha     pt=65536 wire=90188
[PASS] noaead-easy-userloop        + siphash    pt=65536 wire=90192
[PASS] noaead-lowlevel-io          + aes        pt=65536 wire=90176
[PASS] noaead-lowlevel-io          + chacha     pt=65536 wire=90172
[PASS] noaead-lowlevel-io          + siphash    pt=65536 wire=90176
[PASS] noaead-lowlevel-userloop    + aes        pt=65536 wire=90192
[PASS] noaead-lowlevel-userloop    + chacha     pt=65536 wire=90188
[PASS] noaead-lowlevel-userloop    + siphash    pt=65536 wire=90192
[PASS] message-easy-nomac          + aes        pt=1024 wire=4316
[PASS] message-easy-nomac          + chacha     pt=1024 wire=4312
[PASS] message-easy-nomac          + siphash    pt=1024 wire=4316
[PASS] message-easy-auth           + aes        pt=1024 wire=8276
[PASS] message-easy-auth           + chacha     pt=1024 wire=8272
[PASS] message-easy-auth           + siphash    pt=1024 wire=8276
[PASS] message-lowlevel-nomac      + aes        pt=1024 wire=4316
[PASS] message-lowlevel-nomac      + chacha     pt=1024 wire=4312
[PASS] message-lowlevel-nomac      + siphash    pt=1024 wire=4316
[PASS] message-lowlevel-auth       + aes        pt=1024 wire=8276
[PASS] message-lowlevel-auth       + chacha     pt=1024 wire=8272
[PASS] message-lowlevel-auth       + siphash    pt=1024 wire=8276

=== Summary: 30 PASS, 0 FAIL ===

The wire-byte difference between cipher columns is exactly the per-stream nonce-size delta (16 vs 12 vs 16 bytes); the User-Driven Loop variants additionally include 4 bytes of keystream-XORed length prefix per chunk.

Performance

Bench numbers across Single Ouroboros and Triple Ouroboros, message and streaming, encrypt and decrypt (split sub-benches) are tracked in BENCH.md.

Notes on outer cipher key management

The wrapper itself does not address outer key distribution; the examples generate a fresh CSPRNG outer key per run for self-test purposes. In a real deployment the outer key is shared out-of-band (or derived via a separate key-exchange step) and is independent of the ITB seed material. The ITB state blob already carries the inner cipher's keying material; the outer key is the additional piece both endpoints need.

The outer key MAY be reused across many streams provided each stream uses a fresh CSPRNG nonce — this is the standard CTR mode safety contract. The wrapper helpers always generate a fresh nonce internally, so caller-side discipline is reduced to "do not reuse the same (key, nonce) across distinct streams" — a contract the helper enforces by construction.

What this is not

  • Not an integrity layer. The outer cipher is unauthenticated by design — adding a MAC at this layer would defeat the format-deniability goal (the resulting wire would pattern-match an AEAD construction's tag-bearing format, not a generic stream cipher). Use the ITB AEAD path when integrity is required.
  • Not a substitute for ITB's content-deniability. ITB still provides the unconditional content-deniability; the wrap adds format-deniability on top.

Documentation

Overview

Package wrapper provides format-deniability envelopes for ITB ciphertext.

ITB encrypts content into RGBWYOPA pixel containers and provides content-deniability unconditionally — no plaintext bit can be extracted from the wire. However, the ITB wire is parseable by an observer who knows the format: nonce / W / H / container layout for Non-AEAD mode; 32-byte streamID prefix + per-chunk metadata for Streaming AEAD. This package hides the ITB wire pattern under a generic-cipher-looking envelope ("CTR cipher style stream"), so an observer cannot pattern-match ITB-specific signatures (W/H bounds, container layout, streamID prefix for AEAD streaming mode).

This is NOT a random-oracle indistinguishability claim — it is "looks like some other well-known cipher's ciphertext, not specifically ITB". The outer cipher exists for format-deniability ONLY, not for confidentiality (ITB already provides that) and not for integrity (the ITB AEAD path already provides that per chunk and per stream; the ITB Non-AEAD streaming path intentionally has none).

Three outer ciphers are supplied via the Keystream interface:

  • AES-128-CTR (16-byte nonce) — stdlib, AES-NI accelerated.
  • ChaCha20 (RFC8439) (12-byte nonce) — golang.org/x/crypto/chacha20.
  • SipHash-2-4 in CTR mode (16-byte nonce) — small custom PRF construction using github.com/dchest/siphash, sound under the standard PRF assumption SipHash-2-4 already satisfies as a 128-bit-keyed PRF/MAC.

Per-stream nonce hygiene: every Wrap entry point generates a fresh CSPRNG nonce and emits it once at stream start. Within a stream the keystream advances monotonically (CTR counter or ChaCha20 internal counter). This is standard CTR mode usage — not nonce-reuse. Nonce-reuse means two distinct streams using the SAME (key, nonce); avoid that by using a fresh CSPRNG nonce per stream, which every entry point in this package does.

Index

Constants

View Source
const (
	CipherAES128CTR = "aes"
	CipherChaCha20  = "chacha"
	CipherSipHash24 = "siphash"
)

Cipher names accepted by the Make* helpers and the cmd/-flag parsing.

Variables

CipherNames lists every supported outer cipher in iteration order.

Functions

func GenerateKey

func GenerateKey(name string) ([]byte, error)

GenerateKey returns a fresh CSPRNG key sized for the named outer cipher.

func KeySize

func KeySize(name string) (int, error)

KeySize returns the byte length of the key for the named outer cipher.

func NewUnwrapReader

func NewUnwrapReader(name string, key []byte, src io.Reader) (io.Reader, error)

NewUnwrapReader returns an io.Reader that consumes the per-stream nonce from src on construction, then XOR-decrypts every subsequent byte read. Useful when the caller needs an io.Reader to pass to ITB's DecryptStreamIO / DecryptStreamAuthIO, or to read caller-framed chunks emitted through NewWrapWriter back out of the keystream XOR.

func NewWrapWriter

func NewWrapWriter(name string, key []byte, dst io.Writer) (io.Writer, error)

NewWrapWriter returns an io.Writer that emits the per-stream nonce on construction, then XOR-encrypts every subsequent byte through to dst. The matching reader is NewUnwrapReader. Useful when the caller needs an io.Writer to pass to ITB's EncryptStreamIO / EncryptStreamAuthIO, or to drive a user-side loop that emits caller-framed chunks (e.g. a u32_LE length prefix followed by the chunk body) through a single keystream so the framing bytes also pass through the XOR.

func NonceSize

func NonceSize(name string) (int, error)

NonceSize returns the on-wire nonce length for the named outer cipher.

The nonce is emitted as a single prefix per Wrap entry point. AES-CTR uses a 16-byte block-sized IV; ChaCha20 (RFC8439) uses a 12-byte nonce; SipHash-CTR uses a 16-byte construction-defined nonce (the SipHash key is the wrapper key; the nonce gets concatenated with the 64-bit counter under the PRF).

func Unwrap

func Unwrap(name string, key, wire []byte) ([]byte, error)

Unwrap reverses Wrap. The leading nonce is read from wire; the remaining bytes are XOR-decrypted under (key, nonce) and returned.

func UnwrapInPlace

func UnwrapInPlace(name string, key, wire []byte) ([]byte, error)

UnwrapInPlace strips the leading nonce from wire and XORs the remainder in place. Returns an aliased slice equal to wire[NonceSize(name):], fully decrypted. wire is MUTATED.

func Wrap

func Wrap(name string, key, blob []byte) ([]byte, error)

Wrap seals one ITB ciphertext blob under the named outer cipher, emitting the wire form `nonce || keystream-XOR(blob)`. The returned wire bytes are the format-deniability envelope.

func WrapInPlace

func WrapInPlace(name string, key, blob []byte) ([]byte, error)

WrapInPlace XORs blob in place under a fresh outer cipher keystream and returns the per-stream nonce. The caller is expected to emit nonce followed by blob to the wire, or compose a single buffer themselves.

blob is MUTATED. Do not pass plaintext that must be preserved beyond the call. Suitable for hot paths where the caller has just produced an ITB ciphertext and will not re-read it (the typical case for buffered write-to-wire).

Types

type Keystream

type Keystream interface {
	XORKeyStream(dst, src []byte)
}

Keystream is the minimal interface the wrap helpers consume from an outer cipher. The contract matches crypto/cipher.Stream — XORKeyStream xors a keystream segment over src into dst, advancing the internal counter.

All three concrete implementations (AES-128-CTR, ChaCha20, SipHash-CTR) satisfy this signature. The interface stays decoupled from cipher. Stream so the SipHash wrapper does not have to pretend to be a stdlib type.

func MakeKeystream

func MakeKeystream(name string, key, nonce []byte) (Keystream, error)

MakeKeystream constructs an outer cipher Keystream from the named cipher, the caller-provided key, and a per-stream nonce. The key length must equal KeySize(name); the nonce length must equal NonceSize(name).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL