234 lines
6.1 KiB
Go
234 lines
6.1 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
engine_test.go
|
|
|
|
Resolution, round-trip, auto-detect, and capacity tests for the shared engine
|
|
*/
|
|
|
|
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"testing"
|
|
|
|
"github.com/CarterPerez-dev/crypha/internal/carrier/pdf"
|
|
"github.com/CarterPerez-dev/crypha/internal/payload"
|
|
)
|
|
|
|
const (
|
|
qrCoverText = "crypha qr cover"
|
|
textCover = "the quick brown fox jumps over the lazy dog"
|
|
)
|
|
|
|
func pngCover(t *testing.T, w, h int) []byte {
|
|
t.Helper()
|
|
img := image.NewNRGBA(image.Rect(0, 0, w, h))
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
img.SetNRGBA(x, y, color.NRGBA{R: uint8(x), G: uint8(y), B: uint8(x + y), A: 255})
|
|
}
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, img); err != nil {
|
|
t.Fatalf("encode png: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func hideBytes(t *testing.T, format string, cover, plaintext []byte, opts payload.Options) []byte {
|
|
t.Helper()
|
|
var out bytes.Buffer
|
|
if _, err := Hide(HideRequest{
|
|
Format: format,
|
|
Cover: bytes.NewReader(cover),
|
|
Payload: plaintext,
|
|
Out: &out,
|
|
Options: opts,
|
|
}); err != nil {
|
|
t.Fatalf("Hide(%s): %v", format, err)
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
|
|
func TestResolveCarrier(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
format string
|
|
technique string
|
|
wantErr error
|
|
wantFmt string
|
|
}{
|
|
{"image", "image", "", nil, "image"},
|
|
{"pdf default", "pdf", "", nil, "pdf"},
|
|
{"pdf attachment", "pdf", "attachment", nil, "pdf"},
|
|
{"pdf metadata", "pdf", "metadata", nil, "pdf"},
|
|
{"pdf append", "pdf", "append", nil, "pdf"},
|
|
{"pdf bad technique", "pdf", "nonsense", ErrUnknownTechnique, ""},
|
|
{"technique on non-pdf", "image", "attachment", ErrTechniqueOnNonPDF, ""},
|
|
{"unknown format", "nope", "", ErrUnknownFormat, ""},
|
|
{"empty format", "", "", ErrNoFormat, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c, err := ResolveCarrier(tc.format, tc.technique)
|
|
if tc.wantErr != nil {
|
|
if !errors.Is(err, tc.wantErr) {
|
|
t.Fatalf("err = %v, want %v", err, tc.wantErr)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if c.Format() != tc.wantFmt {
|
|
t.Fatalf("format = %q, want %q", c.Format(), tc.wantFmt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlaintextRoundTrip(t *testing.T) {
|
|
cases := []struct {
|
|
format string
|
|
cover []byte
|
|
}{
|
|
{"text", []byte(textCover)},
|
|
{"image", pngCover(t, 64, 64)},
|
|
{"qr", []byte(qrCoverText)},
|
|
}
|
|
secret := []byte("meet at dawn")
|
|
for _, tc := range cases {
|
|
t.Run(tc.format, func(t *testing.T) {
|
|
stego := hideBytes(t, tc.format, tc.cover, secret, payload.Options{})
|
|
res, err := Reveal(RevealRequest{Format: tc.format, Stego: stego})
|
|
if err != nil {
|
|
t.Fatalf("Reveal: %v", err)
|
|
}
|
|
if !bytes.Equal(res.Data, secret) {
|
|
t.Fatalf("data = %q, want %q", res.Data, secret)
|
|
}
|
|
if res.Encrypted {
|
|
t.Fatal("plaintext payload reported as encrypted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncryptedRoundTrip(t *testing.T) {
|
|
cover := pngCover(t, 64, 64)
|
|
secret := []byte("classified")
|
|
pass := []byte("correct horse battery staple")
|
|
stego := hideBytes(t, "image", cover, secret, payload.Options{Passphrase: pass})
|
|
|
|
res, err := Reveal(RevealRequest{Format: "image", Stego: stego, Passphrase: pass})
|
|
if err != nil {
|
|
t.Fatalf("Reveal: %v", err)
|
|
}
|
|
if !bytes.Equal(res.Data, secret) {
|
|
t.Fatalf("data = %q, want %q", res.Data, secret)
|
|
}
|
|
if !res.Encrypted {
|
|
t.Fatal("encrypted payload reported as plaintext")
|
|
}
|
|
|
|
if _, err := Reveal(RevealRequest{Format: "image", Stego: stego}); !errors.Is(err, payload.ErrPassphraseRequired) {
|
|
t.Fatalf("missing passphrase err = %v, want ErrPassphraseRequired", err)
|
|
}
|
|
if _, err := Reveal(RevealRequest{Format: "image", Stego: stego, Passphrase: []byte("wrong")}); !errors.Is(err, payload.ErrDecrypt) {
|
|
t.Fatalf("wrong passphrase err = %v, want ErrDecrypt", err)
|
|
}
|
|
}
|
|
|
|
func TestAutoDetect(t *testing.T) {
|
|
secret := []byte("hi")
|
|
cases := []struct {
|
|
format string
|
|
cover []byte
|
|
}{
|
|
{"text", []byte(textCover)},
|
|
{"image", pngCover(t, 64, 64)},
|
|
{"qr", []byte(qrCoverText)},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.format, func(t *testing.T) {
|
|
stego := hideBytes(t, tc.format, tc.cover, secret, payload.Options{})
|
|
res, err := Reveal(RevealRequest{Stego: stego})
|
|
if err != nil {
|
|
t.Fatalf("auto-detect Reveal: %v", err)
|
|
}
|
|
if res.Format != tc.format {
|
|
t.Fatalf("detected %q, want %q", res.Format, tc.format)
|
|
}
|
|
if !bytes.Equal(res.Data, secret) {
|
|
t.Fatalf("data = %q, want %q", res.Data, secret)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAutoDetectQRNotShadowedByImage(t *testing.T) {
|
|
stego := hideBytes(t, "qr", []byte(qrCoverText), []byte("hi"), payload.Options{})
|
|
res, err := Reveal(RevealRequest{Stego: stego})
|
|
if err != nil {
|
|
t.Fatalf("Reveal: %v", err)
|
|
}
|
|
if res.Format != "qr" {
|
|
t.Fatalf("a qr PNG resolved to %q; the image carrier shadowed qr", res.Format)
|
|
}
|
|
}
|
|
|
|
func TestAutoDetectUndetected(t *testing.T) {
|
|
if _, err := Reveal(RevealRequest{Stego: []byte("just some plain bytes, not a carrier")}); !errors.Is(err, ErrUndetected) {
|
|
t.Fatalf("err = %v, want ErrUndetected", err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogAndTechniques(t *testing.T) {
|
|
cat := Catalog()
|
|
if len(cat) != 5 {
|
|
t.Fatalf("catalog has %d formats, want 5", len(cat))
|
|
}
|
|
for _, fi := range cat {
|
|
if fi.Name == pdf.Format {
|
|
if len(fi.Techniques) != 3 {
|
|
t.Fatalf("pdf techniques = %v, want 3", fi.Techniques)
|
|
}
|
|
} else if fi.Techniques != nil {
|
|
t.Fatalf("%s has techniques %v, want none", fi.Name, fi.Techniques)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCapacityAll(t *testing.T) {
|
|
rows := CapacityAll(pngCover(t, 64, 64))
|
|
var imageRow *CapacityRow
|
|
for i := range rows {
|
|
if rows[i].Format == "image" {
|
|
imageRow = &rows[i]
|
|
}
|
|
}
|
|
if imageRow == nil {
|
|
t.Fatal("no image row in capacity table")
|
|
}
|
|
if imageRow.Err != nil {
|
|
t.Fatalf("image capacity err: %v", imageRow.Err)
|
|
}
|
|
if imageRow.Capacity <= 0 {
|
|
t.Fatalf("image capacity = %d, want positive", imageRow.Capacity)
|
|
}
|
|
}
|
|
|
|
func TestCapacitySingleFormat(t *testing.T) {
|
|
n, err := Capacity("image", bytes.NewReader(pngCover(t, 32, 32)))
|
|
if err != nil {
|
|
t.Fatalf("Capacity: %v", err)
|
|
}
|
|
if n <= 0 {
|
|
t.Fatalf("capacity = %d, want positive", n)
|
|
}
|
|
}
|