494 lines
15 KiB
Go
494 lines
15 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
audio_test.go
|
|
|
|
Round-trip, FLAC-in, rejection, capacity, and sniff tests for the LSB audio carrier
|
|
*/
|
|
|
|
package audio
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/CarterPerez-dev/crypha/internal/carrier"
|
|
"github.com/CarterPerez-dev/crypha/internal/payload"
|
|
goaudio "github.com/go-audio/audio"
|
|
"github.com/go-audio/wav"
|
|
"github.com/mewkiz/flac"
|
|
flacframe "github.com/mewkiz/flac/frame"
|
|
"github.com/mewkiz/flac/meta"
|
|
)
|
|
|
|
const (
|
|
sampleRate = 44100
|
|
coverSlots = 1000
|
|
coverBytes = (coverSlots - lengthPrefixBits) / bitsPerByte
|
|
roomySlots = 8192
|
|
rejectDepth = 8
|
|
minFLACBlock = 16
|
|
)
|
|
|
|
func pseudoSamples(n, seed int) []int {
|
|
out := make([]int, n)
|
|
x := uint32(seed)*2654435761 + 1
|
|
for i := range out {
|
|
x = x*1664525 + 1013904223
|
|
out[i] = int(int16(x >> 16))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func synthWAV(t *testing.T, samples []int, chans int) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
if err := encodeWAV(&buf, pcm{samples: samples, numChannels: chans, sampleRate: sampleRate}); err != nil {
|
|
t.Fatalf("synth wav: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func flacChannels(chans int) flacframe.Channels {
|
|
if chans == 1 {
|
|
return flacframe.ChannelsMono
|
|
}
|
|
return flacframe.ChannelsLR
|
|
}
|
|
|
|
func synthFLAC(t *testing.T, samples []int, chans, rate, bps int) []byte {
|
|
t.Helper()
|
|
perChan := len(samples) / chans
|
|
declaredBlock := perChan
|
|
if declaredBlock < minFLACBlock {
|
|
declaredBlock = minFLACBlock
|
|
}
|
|
info := &meta.StreamInfo{
|
|
SampleRate: uint32(rate),
|
|
NChannels: uint8(chans),
|
|
BitsPerSample: uint8(bps),
|
|
NSamples: uint64(perChan),
|
|
BlockSizeMin: uint16(declaredBlock),
|
|
BlockSizeMax: uint16(declaredBlock),
|
|
}
|
|
var out bytes.Buffer
|
|
enc, err := flac.NewEncoder(&out, info)
|
|
if err != nil {
|
|
t.Fatalf("flac new encoder: %v", err)
|
|
}
|
|
enc.EnablePredictionAnalysis(false)
|
|
|
|
if perChan > 0 {
|
|
subs := make([]*flacframe.Subframe, chans)
|
|
for ch := 0; ch < chans; ch++ {
|
|
s := make([]int32, perChan)
|
|
for i := 0; i < perChan; i++ {
|
|
s[i] = int32(samples[i*chans+ch])
|
|
}
|
|
subs[ch] = &flacframe.Subframe{
|
|
SubHeader: flacframe.SubHeader{Pred: flacframe.PredVerbatim},
|
|
Samples: s,
|
|
NSamples: perChan,
|
|
}
|
|
}
|
|
f := &flacframe.Frame{
|
|
Header: flacframe.Header{
|
|
HasFixedBlockSize: true,
|
|
BlockSize: uint16(perChan),
|
|
SampleRate: uint32(rate),
|
|
Channels: flacChannels(chans),
|
|
BitsPerSample: uint8(bps),
|
|
},
|
|
Subframes: subs,
|
|
}
|
|
if err := enc.WriteFrame(f); err != nil {
|
|
t.Fatalf("flac write frame: %v", err)
|
|
}
|
|
}
|
|
if err := enc.Close(); err != nil {
|
|
t.Fatalf("flac close: %v", err)
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
|
|
func hideReveal(t *testing.T, cover, secret []byte) []byte {
|
|
t.Helper()
|
|
var stego bytes.Buffer
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), secret, &stego); err != nil {
|
|
t.Fatalf("Hide: %v", err)
|
|
}
|
|
got, err := (audioCarrier{}).Reveal(bytes.NewReader(stego.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("Reveal: %v", err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func TestRoundTripWAV(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
chans int
|
|
payload []byte
|
|
}{
|
|
{"mono single byte", 1, []byte{0x42}},
|
|
{"mono text", 1, []byte("crypha audio")},
|
|
{"stereo text", 2, []byte("left and right channels")},
|
|
{"high bits set", 1, bytes.Repeat([]byte{0xFF}, 40)},
|
|
{"binary blob", 2, pseudoBytes(300, 9)},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cover := synthWAV(t, pseudoSamples(roomySlots, 1), tc.chans)
|
|
got := hideReveal(t, cover, tc.payload)
|
|
if !bytes.Equal(got, tc.payload) {
|
|
t.Fatalf("round-trip mismatch: got %x want %x", got, tc.payload)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func pseudoBytes(n, seed int) []byte {
|
|
b := make([]byte, n)
|
|
x := uint32(seed)*2654435761 + 1
|
|
for i := range b {
|
|
x = x*1664525 + 1013904223
|
|
b[i] = byte(x >> 24)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestFLACInWAVOut(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
chans int
|
|
payload []byte
|
|
}{
|
|
{"mono", 1, []byte("decoded from flac, embedded, emitted as wav")},
|
|
{"stereo", 2, pseudoBytes(200, 3)},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cover := synthFLAC(t, pseudoSamples(roomySlots, 2), tc.chans, sampleRate, supportedBitDepth)
|
|
|
|
var stego bytes.Buffer
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), tc.payload, &stego); err != nil {
|
|
t.Fatalf("Hide flac cover: %v", err)
|
|
}
|
|
if !isWAV(stego.Bytes()) {
|
|
t.Fatal("flac cover did not produce a WAV stego output")
|
|
}
|
|
got, err := (audioCarrier{}).Reveal(bytes.NewReader(stego.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("Reveal: %v", err)
|
|
}
|
|
if !bytes.Equal(got, tc.payload) {
|
|
t.Fatalf("flac-in round-trip mismatch: got %x want %x", got, tc.payload)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCapacityBoundary(t *testing.T) {
|
|
cover := synthWAV(t, pseudoSamples(coverSlots, 5), 1)
|
|
|
|
atCap := bytes.Repeat([]byte{0x01}, coverBytes)
|
|
if got := hideReveal(t, cover, atCap); !bytes.Equal(got, atCap) {
|
|
t.Fatal("payload at exact capacity failed to round-trip")
|
|
}
|
|
|
|
overCap := bytes.Repeat([]byte{0x01}, coverBytes+1)
|
|
err := (audioCarrier{}).Hide(bytes.NewReader(cover), overCap, &bytes.Buffer{})
|
|
if err == nil {
|
|
t.Fatal("expected capacity error for oversized payload")
|
|
}
|
|
}
|
|
|
|
func TestCapacityReport(t *testing.T) {
|
|
wavCover := synthWAV(t, pseudoSamples(coverSlots, 7), 1)
|
|
got, err := (audioCarrier{}).Capacity(bytes.NewReader(wavCover))
|
|
if err != nil {
|
|
t.Fatalf("Capacity wav: %v", err)
|
|
}
|
|
if got != coverBytes {
|
|
t.Fatalf("Capacity wav: got %d want %d", got, coverBytes)
|
|
}
|
|
|
|
flacCover := synthFLAC(t, pseudoSamples(coverSlots*2, 8), 2, sampleRate, supportedBitDepth)
|
|
gotFlac, err := (audioCarrier{}).Capacity(bytes.NewReader(flacCover))
|
|
if err != nil {
|
|
t.Fatalf("Capacity flac: %v", err)
|
|
}
|
|
if want := capacityFromSlots(coverSlots * 2); gotFlac != want {
|
|
t.Fatalf("Capacity flac: got %d want %d", gotFlac, want)
|
|
}
|
|
}
|
|
|
|
func synthWAVCustom(t *testing.T, samples []int, chans, bitDepth, audioFormat int) []byte {
|
|
t.Helper()
|
|
ws := &memWriteSeeker{}
|
|
enc := wav.NewEncoder(ws, sampleRate, bitDepth, chans, audioFormat)
|
|
buf := &goaudio.IntBuffer{
|
|
Format: &goaudio.Format{NumChannels: chans, SampleRate: sampleRate},
|
|
Data: samples,
|
|
SourceBitDepth: bitDepth,
|
|
}
|
|
if err := enc.Write(buf); err != nil {
|
|
t.Fatalf("synth custom wav write: %v", err)
|
|
}
|
|
if err := enc.Close(); err != nil {
|
|
t.Fatalf("synth custom wav close: %v", err)
|
|
}
|
|
return ws.buf
|
|
}
|
|
|
|
func TestHideRejectsNonPCMWAV(t *testing.T) {
|
|
cover := synthWAVCustom(t, pseudoSamples(64, 1), 1, supportedBitDepth, 3)
|
|
err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{})
|
|
if err != ErrNotPCM {
|
|
t.Fatalf("expected ErrNotPCM, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHideRejectsWrongBitDepthWAV(t *testing.T) {
|
|
cover := synthWAVCustom(t, pseudoSamples(64, 1), 1, rejectDepth, wavFormatPCM)
|
|
err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{})
|
|
if err != ErrUnsupportedBitDepth {
|
|
t.Fatalf("expected ErrUnsupportedBitDepth, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHideRejectsWrongBitDepthFLAC(t *testing.T) {
|
|
cover := synthFLAC(t, pseudoSamples(64, 4), 1, sampleRate, rejectDepth)
|
|
err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{})
|
|
if err != ErrUnsupportedBitDepth {
|
|
t.Fatalf("expected ErrUnsupportedBitDepth, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHideRejectsUnsupportedFormat(t *testing.T) {
|
|
garbage := []byte("this is not audio at all, just prose")
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(garbage), []byte("x"), &bytes.Buffer{}); err != ErrUnsupportedFormat {
|
|
t.Fatalf("Hide garbage: got %v want ErrUnsupportedFormat", err)
|
|
}
|
|
if _, err := (audioCarrier{}).Capacity(bytes.NewReader(garbage)); err != ErrUnsupportedFormat {
|
|
t.Fatalf("Capacity garbage: got %v want ErrUnsupportedFormat", err)
|
|
}
|
|
}
|
|
|
|
func TestHideEmptyPayloadRejected(t *testing.T) {
|
|
cover := synthWAV(t, pseudoSamples(coverSlots, 1), 1)
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), nil, &bytes.Buffer{}); err != ErrEmptyPayload {
|
|
t.Fatalf("expected ErrEmptyPayload, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRevealRejectsNonWAV(t *testing.T) {
|
|
flacData := synthFLAC(t, pseudoSamples(64, 2), 1, sampleRate, supportedBitDepth)
|
|
if _, err := (audioCarrier{}).Reveal(bytes.NewReader(flacData)); err != ErrUnsupportedFormat {
|
|
t.Fatalf("Reveal flac: got %v want ErrUnsupportedFormat", err)
|
|
}
|
|
if _, err := (audioCarrier{}).Reveal(bytes.NewReader([]byte("garbage"))); err != ErrUnsupportedFormat {
|
|
t.Fatalf("Reveal garbage: got %v want ErrUnsupportedFormat", err)
|
|
}
|
|
}
|
|
|
|
func TestRevealRejectsUndecodableWAV(t *testing.T) {
|
|
cover := synthWAVCustom(t, pseudoSamples(64, 1), 1, supportedBitDepth, 3)
|
|
if _, err := (audioCarrier{}).Reveal(bytes.NewReader(cover)); err != ErrNotPCM {
|
|
t.Fatalf("Reveal float WAV: got %v want ErrNotPCM", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeWAVTruncatedFmt(t *testing.T) {
|
|
var b bytes.Buffer
|
|
b.WriteString("RIFF")
|
|
_ = binary.Write(&b, binary.LittleEndian, uint32(0xFFFFFFFF))
|
|
b.WriteString("WAVE")
|
|
b.WriteString("fmt ")
|
|
_ = binary.Write(&b, binary.LittleEndian, uint32(16))
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(b.Bytes()), []byte("x"), &bytes.Buffer{}); err == nil {
|
|
t.Fatal("expected decode error on a truncated fmt chunk")
|
|
}
|
|
}
|
|
|
|
func TestRevealNoPayload(t *testing.T) {
|
|
clean := make([]int, coverSlots)
|
|
cover := synthWAV(t, clean, 1)
|
|
if _, err := (audioCarrier{}).Reveal(bytes.NewReader(cover)); err != ErrNoPayload {
|
|
t.Fatalf("expected ErrNoPayload on zeroed cover, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRevealTooSmall(t *testing.T) {
|
|
cover := synthWAV(t, pseudoSamples(16, 1), 1)
|
|
if _, err := (audioCarrier{}).Reveal(bytes.NewReader(cover)); err != ErrTooSmall {
|
|
t.Fatalf("expected ErrTooSmall, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSniff(t *testing.T) {
|
|
wavCover := synthWAV(t, pseudoSamples(coverSlots, 1), 1)
|
|
flacCover := synthFLAC(t, pseudoSamples(64, 1), 1, sampleRate, supportedBitDepth)
|
|
cases := []struct {
|
|
name string
|
|
data []byte
|
|
want bool
|
|
}{
|
|
{"wav", wavCover, true},
|
|
{"flac not stego", flacCover, false},
|
|
{"random", []byte("not audio, definitely"), false},
|
|
{"short", []byte("RIFF"), false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := (audioCarrier{}).Sniff(bytes.NewReader(tc.data)); got != tc.want {
|
|
t.Fatalf("Sniff(%s): got %v want %v", tc.name, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncryptedEnvelopeThroughCarrier(t *testing.T) {
|
|
secret := []byte("the drop is behind the third locker")
|
|
envelope, err := payload.Pack(secret, payload.Options{
|
|
Passphrase: []byte("correct horse battery staple"),
|
|
Compress: true,
|
|
Cipher: payload.CipherChaCha20,
|
|
Strength: payload.StrengthDefault,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Pack: %v", err)
|
|
}
|
|
|
|
cover := synthWAV(t, pseudoSamples(roomySlots, 4), 2)
|
|
var stego bytes.Buffer
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), envelope, &stego); err != nil {
|
|
t.Fatalf("Hide envelope: %v", err)
|
|
}
|
|
|
|
recovered, err := (audioCarrier{}).Reveal(bytes.NewReader(stego.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("Reveal envelope: %v", err)
|
|
}
|
|
if !bytes.Equal(recovered, envelope) {
|
|
t.Fatal("carrier did not return the exact envelope bytes")
|
|
}
|
|
|
|
plain, err := payload.Unpack(recovered, []byte("correct horse battery staple"))
|
|
if err != nil {
|
|
t.Fatalf("Unpack: %v", err)
|
|
}
|
|
if !bytes.Equal(plain, secret) {
|
|
t.Fatalf("end-to-end mismatch: got %q want %q", plain, secret)
|
|
}
|
|
}
|
|
|
|
type errReader struct{}
|
|
|
|
func (errReader) Read([]byte) (int, error) {
|
|
return 0, errors.New("forced read failure")
|
|
}
|
|
|
|
func TestReadErrorsPropagate(t *testing.T) {
|
|
if err := (audioCarrier{}).Hide(errReader{}, []byte("x"), &bytes.Buffer{}); err == nil {
|
|
t.Fatal("Hide: expected read error")
|
|
}
|
|
if _, err := (audioCarrier{}).Reveal(errReader{}); err == nil {
|
|
t.Fatal("Reveal: expected read error")
|
|
}
|
|
if _, err := (audioCarrier{}).Capacity(errReader{}); err == nil {
|
|
t.Fatal("Capacity: expected read error")
|
|
}
|
|
}
|
|
|
|
func TestDecodeWAVZeroSamples(t *testing.T) {
|
|
cover := synthWAV(t, nil, 1)
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{}); err != ErrNoSamples {
|
|
t.Fatalf("expected ErrNoSamples on empty WAV, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeWAVMalformedHeader(t *testing.T) {
|
|
cover := []byte("RIFF\x04\x00\x00\x00WAVE")
|
|
err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{})
|
|
if err == nil {
|
|
t.Fatal("expected decode error on a RIFF/WAVE file with no fmt chunk")
|
|
}
|
|
}
|
|
|
|
func TestDecodeFLACZeroFrames(t *testing.T) {
|
|
cover := synthFLAC(t, nil, 1, sampleRate, supportedBitDepth)
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{}); err != ErrNoSamples {
|
|
t.Fatalf("expected ErrNoSamples on frameless FLAC, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeFLACGarbage(t *testing.T) {
|
|
cover := append([]byte("fLaC"), pseudoBytes(64, 11)...)
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(cover), []byte("x"), &bytes.Buffer{}); err == nil {
|
|
t.Fatal("expected decode error on fLaC-tagged garbage")
|
|
}
|
|
}
|
|
|
|
func TestDecodeFLACTruncatedFrame(t *testing.T) {
|
|
full := synthFLAC(t, pseudoSamples(roomySlots, 6), 1, sampleRate, supportedBitDepth)
|
|
truncated := full[:len(full)-16]
|
|
if err := (audioCarrier{}).Hide(bytes.NewReader(truncated), []byte("x"), &bytes.Buffer{}); err == nil {
|
|
t.Fatal("expected decode error on a FLAC truncated mid-frame")
|
|
}
|
|
}
|
|
|
|
func TestCapacityTinyCoverIsZero(t *testing.T) {
|
|
cover := synthWAV(t, pseudoSamples(16, 1), 1)
|
|
got, err := (audioCarrier{}).Capacity(bytes.NewReader(cover))
|
|
if err != nil {
|
|
t.Fatalf("Capacity tiny: %v", err)
|
|
}
|
|
if got != 0 {
|
|
t.Fatalf("Capacity tiny: got %d want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestMemWriteSeeker(t *testing.T) {
|
|
m := &memWriteSeeker{}
|
|
if _, err := m.Write([]byte("hello world")); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
if pos, err := m.Seek(6, io.SeekStart); err != nil || pos != 6 {
|
|
t.Fatalf("SeekStart: pos=%d err=%v", pos, err)
|
|
}
|
|
if pos, err := m.Seek(2, io.SeekCurrent); err != nil || pos != 8 {
|
|
t.Fatalf("SeekCurrent: pos=%d err=%v", pos, err)
|
|
}
|
|
if pos, err := m.Seek(-5, io.SeekEnd); err != nil || pos != 6 {
|
|
t.Fatalf("SeekEnd: pos=%d err=%v", pos, err)
|
|
}
|
|
if _, err := m.Seek(-1, io.SeekStart); err != errNegativeSeek {
|
|
t.Fatalf("negative seek: got %v want errNegativeSeek", err)
|
|
}
|
|
if _, err := m.Seek(0, 99); err != errInvalidWhence {
|
|
t.Fatalf("bad whence: got %v want errInvalidWhence", err)
|
|
}
|
|
if _, err := m.Seek(0, io.SeekStart); err != nil {
|
|
t.Fatalf("reset seek: %v", err)
|
|
}
|
|
if _, err := m.Write([]byte("HELLO")); err != nil {
|
|
t.Fatalf("overwrite: %v", err)
|
|
}
|
|
if !bytes.Equal(m.buf, []byte("HELLO world")) {
|
|
t.Fatalf("overwrite mismatch: %q", m.buf)
|
|
}
|
|
}
|
|
|
|
func TestRegisteredInRegistry(t *testing.T) {
|
|
c, ok := carrier.Get(Format)
|
|
if !ok {
|
|
t.Fatal("audio carrier did not self-register")
|
|
}
|
|
if c.Format() != Format {
|
|
t.Fatalf("registry returned wrong carrier: %s", c.Format())
|
|
}
|
|
}
|