Cybersecurity-Projects/PROJECTS/beginner/steganography-multi-tool/internal/carrier/pdf/pdf_test.go

396 lines
11 KiB
Go

/*
©AngelaMos | 2026
pdf_test.go
Per-technique round-trip, auto-detect, capacity, ordering, and sniff tests for the PDF carrier
*/
package pdf
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/CarterPerez-dev/crypha/internal/carrier"
"github.com/CarterPerez-dev/crypha/internal/payload"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)
const pageJSON = `{"pages": {"1": {"content": {}}}}`
var allTechniques = []Technique{TechniqueAttachment, TechniqueMetadata, TechniqueAppend}
func minimalPDF(t *testing.T) []byte {
t.Helper()
var buf bytes.Buffer
if err := api.Create(nil, strings.NewReader(pageJSON), &buf, newConfig()); err != nil {
t.Fatalf("create demo pdf: %v", err)
}
return buf.Bytes()
}
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 hideWith(t *testing.T, tech Technique, cover, payload []byte) []byte {
t.Helper()
var stego bytes.Buffer
if err := New(tech).Hide(bytes.NewReader(cover), payload, &stego); err != nil {
t.Fatalf("Hide(%s): %v", tech, err)
}
return stego.Bytes()
}
func TestFixtureIsValidPDF(t *testing.T) {
cover := minimalPDF(t)
if !isPDF(cover) {
t.Fatal("fixture is not a PDF")
}
}
func TestRoundTripTechniques(t *testing.T) {
cover := minimalPDF(t)
payloads := []struct {
name string
data []byte
}{
{"short text", []byte("meet at the docks")},
{"single byte", []byte{0x42}},
{"binary blob", pseudoBytes(500, 7)},
}
for _, tech := range allTechniques {
for _, pl := range payloads {
t.Run(string(tech)+"/"+pl.name, func(t *testing.T) {
stego := hideWith(t, tech, cover, pl.data)
got, err := New(tech).Reveal(bytes.NewReader(stego))
if err != nil {
t.Fatalf("Reveal(%s): %v", tech, err)
}
if !bytes.Equal(got, pl.data) {
t.Fatalf("%s round-trip mismatch: got %x want %x", tech, got, pl.data)
}
})
}
}
}
func TestRevealAutoDetect(t *testing.T) {
cover := minimalPDF(t)
detector, ok := carrier.Get(Format)
if !ok {
t.Fatal("pdf carrier not registered")
}
for _, tech := range allTechniques {
t.Run(string(tech), func(t *testing.T) {
payload := []byte("auto-detect me: " + string(tech))
stego := hideWith(t, tech, cover, payload)
got, err := detector.Reveal(bytes.NewReader(stego))
if err != nil {
t.Fatalf("auto-detect Reveal for %s: %v", tech, err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("auto-detect mismatch for %s: got %q want %q", tech, got, payload)
}
})
}
}
func TestMetadataChunking(t *testing.T) {
cover := minimalPDF(t)
payload := pseudoBytes(metaChunkSize, 3)
stego := hideWith(t, TechniqueMetadata, cover, payload)
got, err := New(TechniqueMetadata).Reveal(bytes.NewReader(stego))
if err != nil {
t.Fatalf("Reveal: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatal("multi-chunk metadata round-trip mismatch")
}
}
func TestAppendStrippedByPdfcpuRewrite(t *testing.T) {
cover := minimalPDF(t)
appendStego := hideWith(t, TechniqueAppend, cover, []byte("fragile append payload"))
if _, ok := revealAppend(appendStego); !ok {
t.Fatal("append payload not present before rewrite")
}
rewritten := hideWith(t, TechniqueAttachment, appendStego, []byte("new attachment"))
if _, ok := revealAppend(rewritten); ok {
t.Fatal("expected pdfcpu rewrite to strip the trailing append payload")
}
if _, ok := revealAttachment(rewritten); !ok {
t.Fatal("attachment payload should survive the rewrite")
}
}
func TestCapacityUnbounded(t *testing.T) {
cover := minimalPDF(t)
got, err := New(TechniqueAttachment).Capacity(bytes.NewReader(cover))
if err != nil {
t.Fatalf("Capacity: %v", err)
}
if got != unboundedCapacity {
t.Fatalf("Capacity: got %d want %d", got, unboundedCapacity)
}
}
func TestSniff(t *testing.T) {
cover := minimalPDF(t)
cases := []struct {
name string
data []byte
want bool
}{
{"pdf", cover, true},
{"random", []byte("not a pdf at all, just text"), false},
{"short", []byte("%PD"), false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := New(TechniqueAttachment).Sniff(bytes.NewReader(tc.data)); got != tc.want {
t.Fatalf("Sniff(%s): got %v want %v", tc.name, got, tc.want)
}
})
}
}
func TestRejectNonPDF(t *testing.T) {
garbage := []byte("this is plainly not a pdf document")
if err := New(TechniqueAttachment).Hide(bytes.NewReader(garbage), []byte("x"), &bytes.Buffer{}); err != ErrUnsupportedFormat {
t.Fatalf("Hide non-pdf: got %v want ErrUnsupportedFormat", err)
}
if _, err := New(TechniqueAttachment).Reveal(bytes.NewReader(garbage)); err != ErrUnsupportedFormat {
t.Fatalf("Reveal non-pdf: got %v want ErrUnsupportedFormat", err)
}
if _, err := New(TechniqueAttachment).Capacity(bytes.NewReader(garbage)); err != ErrUnsupportedFormat {
t.Fatalf("Capacity non-pdf: got %v want ErrUnsupportedFormat", err)
}
}
func TestEmptyPayloadRejected(t *testing.T) {
cover := minimalPDF(t)
if err := New(TechniqueAttachment).Hide(bytes.NewReader(cover), nil, &bytes.Buffer{}); err != ErrEmptyPayload {
t.Fatalf("expected ErrEmptyPayload, got %v", err)
}
}
func TestRevealNoPayload(t *testing.T) {
cover := minimalPDF(t)
if _, err := New(TechniqueAttachment).Reveal(bytes.NewReader(cover)); err != ErrNoPayload {
t.Fatalf("expected ErrNoPayload on a clean PDF, got %v", err)
}
}
type errReader struct{}
func (errReader) Read([]byte) (int, error) {
return 0, errors.New("forced read failure")
}
func TestReadErrorsPropagate(t *testing.T) {
if err := New(TechniqueAttachment).Hide(errReader{}, []byte("x"), &bytes.Buffer{}); err == nil {
t.Fatal("Hide: expected read error")
}
if _, err := New(TechniqueAttachment).Reveal(errReader{}); err == nil {
t.Fatal("Reveal: expected read error")
}
if _, err := New(TechniqueAttachment).Capacity(errReader{}); err == nil {
t.Fatal("Capacity: expected read error")
}
}
func TestEncryptedEnvelopeThroughCarrier(t *testing.T) {
cover := minimalPDF(t)
secret := []byte("the account number is 4417 1234 5678 9012")
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)
}
for _, tech := range allTechniques {
t.Run(string(tech), func(t *testing.T) {
stego := hideWith(t, tech, cover, envelope)
recovered, err := New(tech).Reveal(bytes.NewReader(stego))
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)
}
})
}
}
func TestCorruptPDFRejectedByTechniques(t *testing.T) {
corrupt := []byte("%PDF-1.7\nnot a real pdf body, no xref, no objects\n%%EOF")
if err := New(TechniqueAttachment).Hide(bytes.NewReader(corrupt), []byte("x"), &bytes.Buffer{}); err == nil {
t.Fatal("Hide attachment on corrupt PDF: expected error")
}
if err := New(TechniqueMetadata).Hide(bytes.NewReader(corrupt), []byte("x"), &bytes.Buffer{}); err == nil {
t.Fatal("Hide metadata on corrupt PDF: expected error")
}
if _, err := New(TechniqueAttachment).Reveal(bytes.NewReader(corrupt)); err != ErrNoPayload {
t.Fatalf("Reveal corrupt PDF: got %v want ErrNoPayload", err)
}
}
func TestForeignAttachmentIgnored(t *testing.T) {
cover := minimalPDF(t)
conf := newConfig()
ctx, err := api.ReadValidateAndOptimize(bytes.NewReader(cover), conf)
if err != nil {
t.Fatalf("read cover: %v", err)
}
other := model.Attachment{
Reader: bytes.NewReader([]byte("someone else's file")),
ID: "other.txt",
FileName: "other.txt",
ModTime: &epoch,
}
if err := ctx.AddAttachment(other, false); err != nil {
t.Fatalf("add foreign attachment: %v", err)
}
var buf bytes.Buffer
if err := api.Write(ctx, &buf, conf); err != nil {
t.Fatalf("write: %v", err)
}
if _, ok := revealAttachment(buf.Bytes()); ok {
t.Fatal("revealAttachment must ignore a non-crypha attachment")
}
}
type errWriter struct {
failAt int
count int
}
func (w *errWriter) Write(p []byte) (int, error) {
w.count++
if w.count > w.failAt {
return 0, errors.New("forced write failure")
}
return len(p), nil
}
func TestAppendWriteErrorsPropagate(t *testing.T) {
cover := minimalPDF(t)
for failAt := 0; failAt < 4; failAt++ {
if err := hideAppend(cover, []byte("payload"), &errWriter{failAt: failAt}); err == nil {
t.Fatalf("hideAppend failAt=%d: expected write error", failAt)
}
}
}
func TestAttachmentWriteErrorPropagates(t *testing.T) {
cover := minimalPDF(t)
if err := hideAttachment(cover, []byte("payload"), &errWriter{failAt: 0}); err == nil {
t.Fatal("hideAttachment: expected write error")
}
}
func metaStego(t *testing.T, cover []byte, props map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
if err := api.AddProperties(bytes.NewReader(cover), &buf, props, newConfig()); err != nil {
t.Fatalf("add properties: %v", err)
}
return buf.Bytes()
}
func TestMetadataTamperedRejected(t *testing.T) {
cover := minimalPDF(t)
cases := []struct {
name string
props map[string]string
}{
{"non-numeric count", map[string]string{metaCountKey: "not-a-number"}},
{"zero count", map[string]string{metaCountKey: "0"}},
{"missing chunk", map[string]string{metaCountKey: "1"}},
{"invalid base64", map[string]string{metaCountKey: "1", metaKeyPrefix + "0": "@@@not-base64@@@"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
stego := metaStego(t, cover, tc.props)
if _, ok := revealMetadata(stego); ok {
t.Fatalf("revealMetadata must reject %s", tc.name)
}
})
}
}
func TestEmptyMetadataYieldsNoPayload(t *testing.T) {
cover := minimalPDF(t)
stego := metaStego(t, cover, map[string]string{metaCountKey: "0"})
if _, err := New(TechniqueAttachment).Reveal(bytes.NewReader(stego)); err != ErrNoPayload {
t.Fatalf("count=0 metadata: got %v want ErrNoPayload", err)
}
}
func TestEmptyMetadataDoesNotMaskAppend(t *testing.T) {
cover := minimalPDF(t)
secret := []byte("the real secret rides under a spurious empty-metadata key")
metaPart := metaStego(t, cover, map[string]string{metaCountKey: "0"})
var stego bytes.Buffer
if err := hideAppend(metaPart, secret, &stego); err != nil {
t.Fatalf("hideAppend: %v", err)
}
got, err := New(TechniqueAttachment).Reveal(bytes.NewReader(stego.Bytes()))
if err != nil {
t.Fatalf("Reveal: %v", err)
}
if !bytes.Equal(got, secret) {
t.Fatalf("empty metadata masked the append payload: got %q", got)
}
}
func TestRevealAppendEdgeCases(t *testing.T) {
if _, ok := revealAppend([]byte("tiny")); ok {
t.Fatal("revealAppend on too-short input must return false")
}
cover := minimalPDF(t)
payload := []byte("magic mismatch probe")
stego := hideWith(t, TechniqueAppend, cover, payload)
magicPos := len(stego) - lengthPrefixBytes - len(payload) - len(appendMagic)
corrupt := append([]byte{}, stego...)
corrupt[magicPos] ^= 0xFF
if _, ok := revealAppend(corrupt); ok {
t.Fatal("revealAppend must reject a mismatched magic")
}
}
func TestRegisteredInRegistry(t *testing.T) {
c, ok := carrier.Get(Format)
if !ok {
t.Fatal("pdf carrier did not self-register")
}
if c.Format() != Format {
t.Fatalf("registry returned wrong carrier: %s", c.Format())
}
}