Cybersecurity-Projects/PROJECTS/beginner/steganography-multi-tool/internal/engine/engine_test.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)
}
}