Merge stegasoo (v4.3.0, steganography) and verisoo (v0.1.0, attestation) as subpackages under soosef.stegasoo and soosef.verisoo. This eliminates cross-repo coordination and enables atomic changes across the full stack. - Copy stegasoo (34 modules) and verisoo (15 modules) into src/soosef/ - Convert all verisoo absolute imports to relative imports - Rewire ~50 import sites across soosef code (cli, web, keystore, tests) - Replace stegasoo/verisoo pip deps with inlined code + pip extras (stego-dct, stego-audio, attest, web, api, cli, fieldkit, all, dev) - Add _availability.py for runtime feature detection - Add unified FastAPI mount point at soosef.api - Copy and adapt tests from both repos (155 pass, 1 skip) - Drop standalone CLI/web frontends; keep FastAPI as optional modules - Both source repos tagged pre-monorepo-consolidation on GitHub Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
863 lines
30 KiB
Python
863 lines
30 KiB
Python
"""
|
|
Tests for Stegasoo audio steganography.
|
|
|
|
Tests cover:
|
|
- Audio LSB roundtrip (encode + decode)
|
|
- Audio spread spectrum roundtrip (v0 legacy + v2 per-channel)
|
|
- Wrong credentials fail to decode
|
|
- Capacity calculations (per-tier)
|
|
- Format detection
|
|
- Audio validation
|
|
- Per-channel stereo/multichannel embedding (v4.4.0)
|
|
- Chip tier roundtrips (v4.4.0)
|
|
- LFE channel skipping (v4.4.0)
|
|
- Backward compat: v0 decode from v2 code
|
|
- Header v2 build/parse roundtrip
|
|
- Round-robin bit distribution
|
|
"""
|
|
|
|
import io
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import soundfile as sf
|
|
|
|
from soosef.stegasoo.constants import AUDIO_ENABLED, EMBED_MODE_AUDIO_LSB, EMBED_MODE_AUDIO_SPREAD
|
|
from soosef.stegasoo.models import AudioCapacityInfo, AudioEmbedStats, AudioInfo
|
|
|
|
pytestmark = pytest.mark.skipif(not AUDIO_ENABLED, reason="Audio support disabled (STEGASOO_AUDIO)")
|
|
|
|
# Path to real test data files
|
|
_TEST_DATA = Path(__file__).parent.parent / "test_data"
|
|
_REFERENCE_PNG = _TEST_DATA / "reference.png"
|
|
_SPEECH_WAV = _TEST_DATA / "stupid_elitist_speech.wav"
|
|
|
|
|
|
# =============================================================================
|
|
# FIXTURES
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def carrier_wav() -> bytes:
|
|
"""Generate a small test WAV file (1 second, 44100 Hz, mono, 16-bit)."""
|
|
sample_rate = 44100
|
|
duration = 1.0
|
|
num_samples = int(sample_rate * duration)
|
|
t = np.linspace(0, duration, num_samples, endpoint=False)
|
|
samples = (np.sin(2 * np.pi * 440 * t) * 16000).astype(np.int16)
|
|
|
|
buf = io.BytesIO()
|
|
sf.write(buf, samples, sample_rate, format="WAV", subtype="PCM_16")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def carrier_wav_stereo() -> bytes:
|
|
"""Generate a stereo test WAV file (5 seconds for spread spectrum capacity)."""
|
|
sample_rate = 44100
|
|
duration = 5.0
|
|
num_samples = int(sample_rate * duration)
|
|
t = np.linspace(0, duration, num_samples, endpoint=False)
|
|
left = (np.sin(2 * np.pi * 440 * t) * 16000).astype(np.int16)
|
|
right = (np.sin(2 * np.pi * 880 * t) * 16000).astype(np.int16)
|
|
samples = np.column_stack([left, right])
|
|
|
|
buf = io.BytesIO()
|
|
sf.write(buf, samples, sample_rate, format="WAV", subtype="PCM_16")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def carrier_wav_long() -> bytes:
|
|
"""Generate a longer WAV (15 seconds) for spread spectrum tests."""
|
|
sample_rate = 44100
|
|
duration = 15.0
|
|
num_samples = int(sample_rate * duration)
|
|
t = np.linspace(0, duration, num_samples, endpoint=False)
|
|
samples = (
|
|
(np.sin(2 * np.pi * 440 * t) + np.sin(2 * np.pi * 880 * t) + np.sin(2 * np.pi * 1320 * t))
|
|
* 5000
|
|
).astype(np.int16)
|
|
|
|
buf = io.BytesIO()
|
|
sf.write(buf, samples, sample_rate, format="WAV", subtype="PCM_16")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def carrier_wav_stereo_long() -> bytes:
|
|
"""Generate a stereo WAV (15 seconds) for per-channel spread tests."""
|
|
sample_rate = 48000
|
|
duration = 15.0
|
|
num_samples = int(sample_rate * duration)
|
|
t = np.linspace(0, duration, num_samples, endpoint=False)
|
|
left = (np.sin(2 * np.pi * 440 * t) * 10000).astype(np.float64) / 32768.0
|
|
right = (np.sin(2 * np.pi * 660 * t) * 10000).astype(np.float64) / 32768.0
|
|
samples = np.column_stack([left, right])
|
|
|
|
buf = io.BytesIO()
|
|
sf.write(buf, samples, sample_rate, format="WAV", subtype="PCM_16")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def carrier_wav_5_1() -> bytes:
|
|
"""Generate a 6-channel (5.1) WAV for LFE skip tests."""
|
|
sample_rate = 48000
|
|
duration = 15.0
|
|
num_samples = int(sample_rate * duration)
|
|
t = np.linspace(0, duration, num_samples, endpoint=False)
|
|
|
|
# 6 channels with different frequencies
|
|
freqs = [440, 554, 660, 80, 880, 1100] # ch3 = LFE (low freq)
|
|
channels = []
|
|
for freq in freqs:
|
|
ch = (np.sin(2 * np.pi * freq * t) * 8000).astype(np.float64) / 32768.0
|
|
channels.append(ch)
|
|
samples = np.column_stack(channels)
|
|
|
|
buf = io.BytesIO()
|
|
sf.write(buf, samples, sample_rate, format="WAV", subtype="PCM_16")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def carrier_wav_spread_integration() -> bytes:
|
|
"""Generate a very long WAV (150 seconds) for spread spectrum integration tests."""
|
|
sample_rate = 44100
|
|
duration = 150.0
|
|
num_samples = int(sample_rate * duration)
|
|
t = np.linspace(0, duration, num_samples, endpoint=False)
|
|
samples = (
|
|
(np.sin(2 * np.pi * 440 * t) + np.sin(2 * np.pi * 880 * t) + np.sin(2 * np.pi * 1320 * t))
|
|
* 5000
|
|
).astype(np.int16)
|
|
|
|
buf = io.BytesIO()
|
|
sf.write(buf, samples, sample_rate, format="WAV", subtype="PCM_16")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def reference_photo() -> bytes:
|
|
"""Load real reference photo from test_data, or generate a small one."""
|
|
if _REFERENCE_PNG.exists():
|
|
return _REFERENCE_PNG.read_bytes()
|
|
from PIL import Image
|
|
|
|
img = Image.new("RGB", (100, 100), color=(128, 64, 32))
|
|
buf = io.BytesIO()
|
|
img.save(buf, "PNG")
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def speech_wav() -> bytes:
|
|
"""Load real speech WAV from test_data (48kHz mono, ~68s)."""
|
|
if not _SPEECH_WAV.exists():
|
|
pytest.skip("test_data/stupid_elitist_speech.wav not found")
|
|
return _SPEECH_WAV.read_bytes()
|
|
|
|
|
|
# =============================================================================
|
|
# AUDIO LSB TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestAudioLSB:
|
|
"""Tests for audio LSB steganography."""
|
|
|
|
def test_calculate_capacity(self, carrier_wav):
|
|
from soosef.stegasoo.audio_steganography import calculate_audio_lsb_capacity
|
|
|
|
capacity = calculate_audio_lsb_capacity(carrier_wav)
|
|
assert capacity > 0
|
|
# 1 second at 44100 Hz mono should give ~5KB capacity at 1 bit/sample
|
|
assert capacity > 4000
|
|
|
|
def test_embed_extract_roundtrip(self, carrier_wav):
|
|
"""Test basic LSB embed/extract roundtrip."""
|
|
from soosef.stegasoo.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
|
|
|
|
payload = b"Hello, audio steganography!"
|
|
key = b"\x42" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_lsb(payload, carrier_wav, key)
|
|
|
|
assert isinstance(stats, AudioEmbedStats)
|
|
assert stats.embed_mode == EMBED_MODE_AUDIO_LSB
|
|
assert stats.bytes_embedded > 0
|
|
assert stats.samples_modified > 0
|
|
assert 0 < stats.capacity_used <= 1.0
|
|
|
|
extracted = extract_from_audio_lsb(stego_audio, key)
|
|
assert extracted is not None
|
|
assert extracted == payload
|
|
|
|
def test_embed_extract_stereo(self, carrier_wav_stereo):
|
|
"""Test LSB roundtrip with stereo audio."""
|
|
from soosef.stegasoo.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
|
|
|
|
payload = b"Stereo test message"
|
|
key = b"\xAB" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_lsb(payload, carrier_wav_stereo, key)
|
|
assert stats.channels == 2
|
|
|
|
extracted = extract_from_audio_lsb(stego_audio, key)
|
|
assert extracted == payload
|
|
|
|
def test_wrong_key_fails(self, carrier_wav):
|
|
"""Test that wrong key produces no valid extraction."""
|
|
from soosef.stegasoo.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
|
|
|
|
payload = b"Secret message"
|
|
correct_key = b"\x42" * 32
|
|
wrong_key = b"\xFF" * 32
|
|
|
|
stego_audio, _ = embed_in_audio_lsb(payload, carrier_wav, correct_key)
|
|
|
|
extracted = extract_from_audio_lsb(stego_audio, wrong_key)
|
|
assert extracted is None or extracted != payload
|
|
|
|
def test_two_bits_per_sample(self, carrier_wav):
|
|
"""Test embedding with 2 bits per sample."""
|
|
from soosef.stegasoo.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
|
|
|
|
payload = b"Two bits per sample test"
|
|
key = b"\x55" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_lsb(payload, carrier_wav, key, bits_per_sample=2)
|
|
|
|
extracted = extract_from_audio_lsb(stego_audio, key, bits_per_sample=2)
|
|
assert extracted == payload
|
|
|
|
def test_generate_sample_indices(self):
|
|
"""Test deterministic sample index generation."""
|
|
from soosef.stegasoo.audio_steganography import generate_sample_indices
|
|
|
|
key = b"\x42" * 32
|
|
indices1 = generate_sample_indices(key, 10000, 100)
|
|
indices2 = generate_sample_indices(key, 10000, 100)
|
|
|
|
assert indices1 == indices2
|
|
assert all(0 <= i < 10000 for i in indices1)
|
|
assert len(set(indices1)) == len(indices1)
|
|
|
|
|
|
# =============================================================================
|
|
# AUDIO SPREAD SPECTRUM TESTS (v2 per-channel)
|
|
# =============================================================================
|
|
|
|
|
|
class TestAudioSpread:
|
|
"""Tests for audio spread spectrum steganography (v2 per-channel)."""
|
|
|
|
def test_calculate_capacity_default_tier(self, carrier_wav_long):
|
|
from soosef.stegasoo.spread_steganography import calculate_audio_spread_capacity
|
|
|
|
capacity = calculate_audio_spread_capacity(carrier_wav_long)
|
|
assert isinstance(capacity, AudioCapacityInfo)
|
|
assert capacity.usable_capacity_bytes > 0
|
|
assert capacity.embed_mode == EMBED_MODE_AUDIO_SPREAD
|
|
assert capacity.chip_tier == 2 # default
|
|
assert capacity.chip_length == 1024
|
|
|
|
def test_calculate_capacity_per_tier(self, carrier_wav_long):
|
|
"""Capacity should increase as chip length decreases."""
|
|
from soosef.stegasoo.spread_steganography import calculate_audio_spread_capacity
|
|
|
|
cap_lossless = calculate_audio_spread_capacity(carrier_wav_long, chip_tier=0)
|
|
cap_high = calculate_audio_spread_capacity(carrier_wav_long, chip_tier=1)
|
|
cap_low = calculate_audio_spread_capacity(carrier_wav_long, chip_tier=2)
|
|
|
|
assert cap_lossless.chip_length == 256
|
|
assert cap_high.chip_length == 512
|
|
assert cap_low.chip_length == 1024
|
|
|
|
# Smaller chip = more capacity
|
|
assert cap_lossless.usable_capacity_bytes > cap_high.usable_capacity_bytes
|
|
assert cap_high.usable_capacity_bytes > cap_low.usable_capacity_bytes
|
|
|
|
def test_spread_roundtrip_default_tier(self, carrier_wav_long):
|
|
"""Test spread spectrum embed/extract roundtrip (default tier 2)."""
|
|
from soosef.stegasoo.spread_steganography import (
|
|
embed_in_audio_spread,
|
|
extract_from_audio_spread,
|
|
)
|
|
|
|
payload = b"Spread test v2"
|
|
seed = b"\x42" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_spread(payload, carrier_wav_long, seed)
|
|
|
|
assert isinstance(stats, AudioEmbedStats)
|
|
assert stats.embed_mode == EMBED_MODE_AUDIO_SPREAD
|
|
assert stats.chip_tier == 2
|
|
assert stats.chip_length == 1024
|
|
|
|
extracted = extract_from_audio_spread(stego_audio, seed)
|
|
assert extracted is not None
|
|
assert extracted == payload
|
|
|
|
def test_spread_roundtrip_tier_0(self, carrier_wav_long):
|
|
"""Test spread spectrum at tier 0 (chip=256, lossless)."""
|
|
from soosef.stegasoo.spread_steganography import (
|
|
embed_in_audio_spread,
|
|
extract_from_audio_spread,
|
|
)
|
|
|
|
payload = b"Lossless tier test with more data to embed for coverage"
|
|
seed = b"\x42" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_spread(payload, carrier_wav_long, seed, chip_tier=0)
|
|
assert stats.chip_tier == 0
|
|
assert stats.chip_length == 256
|
|
|
|
extracted = extract_from_audio_spread(stego_audio, seed)
|
|
assert extracted is not None
|
|
assert extracted == payload
|
|
|
|
def test_spread_roundtrip_tier_1(self, carrier_wav_long):
|
|
"""Test spread spectrum at tier 1 (chip=512, high lossy)."""
|
|
from soosef.stegasoo.spread_steganography import (
|
|
embed_in_audio_spread,
|
|
extract_from_audio_spread,
|
|
)
|
|
|
|
payload = b"High lossy tier test"
|
|
seed = b"\x42" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_spread(payload, carrier_wav_long, seed, chip_tier=1)
|
|
assert stats.chip_tier == 1
|
|
assert stats.chip_length == 512
|
|
|
|
extracted = extract_from_audio_spread(stego_audio, seed)
|
|
assert extracted is not None
|
|
assert extracted == payload
|
|
|
|
def test_wrong_seed_fails(self, carrier_wav_long):
|
|
"""Test that wrong seed produces no valid extraction."""
|
|
from soosef.stegasoo.spread_steganography import (
|
|
embed_in_audio_spread,
|
|
extract_from_audio_spread,
|
|
)
|
|
|
|
payload = b"Secret spread"
|
|
correct_seed = b"\x42" * 32
|
|
wrong_seed = b"\xFF" * 32
|
|
|
|
stego_audio, _ = embed_in_audio_spread(payload, carrier_wav_long, correct_seed)
|
|
|
|
extracted = extract_from_audio_spread(stego_audio, wrong_seed)
|
|
assert extracted is None or extracted != payload
|
|
|
|
def test_per_channel_stereo_roundtrip(self, carrier_wav_stereo_long):
|
|
"""Test that stereo per-channel embedding/extraction works."""
|
|
from soosef.stegasoo.spread_steganography import (
|
|
embed_in_audio_spread,
|
|
extract_from_audio_spread,
|
|
)
|
|
|
|
payload = b"Stereo per-channel test"
|
|
seed = b"\xAB" * 32
|
|
|
|
stego_audio, stats = embed_in_audio_spread(
|
|
payload, carrier_wav_stereo_long, seed, chip_tier=0
|
|
)
|
|
assert stats.channels == 2
|
|
assert stats.embeddable_channels == 2
|
|
|
|
extracted = extract_from_audio_spread(stego_audio, seed)
|
|
assert extracted is not None
|
|
assert extracted == payload
|
|
|
|
def test_per_channel_preserves_spatial_mix(self, carrier_wav_stereo_long):
|
|
"""Verify that per-channel embedding doesn't destroy the spatial mix.
|
|
|
|
The difference between left and right channels should be preserved
|
|
(not zeroed out as the old mono-broadcast approach would do).
|
|
"""
|
|
from soosef.stegasoo.spread_steganography import embed_in_audio_spread
|
|
|
|
payload = b"Spatial preservation test"
|
|
seed = b"\xCD" * 32
|
|
|
|
# Read original
|
|
orig_samples, _ = sf.read(io.BytesIO(carrier_wav_stereo_long), dtype="float64", always_2d=True)
|
|
orig_diff = orig_samples[:, 0] - orig_samples[:, 1]
|
|
|
|
# Embed
|
|
stego_bytes, _ = embed_in_audio_spread(
|
|
payload, carrier_wav_stereo_long, seed, chip_tier=0
|
|
)
|
|
|
|
# Read stego
|
|
stego_samples, _ = sf.read(io.BytesIO(stego_bytes), dtype="float64", always_2d=True)
|
|
stego_diff = stego_samples[:, 0] - stego_samples[:, 1]
|
|
|
|
# The channel difference should not be identical (embedding adds different
|
|
# noise per channel), but should be very close (embedding is subtle)
|
|
# With the old mono-broadcast approach, stego_diff would equal orig_diff
|
|
# exactly in unmodified regions but differ where data was embedded.
|
|
# With per-channel, both channels get independent modifications.
|
|
correlation = np.corrcoef(orig_diff, stego_diff)[0, 1]
|
|
assert correlation > 0.95, f"Spatial mix correlation too low: {correlation}"
|
|
|
|
def test_capacity_scales_with_channels(self, carrier_wav_long, carrier_wav_stereo_long):
|
|
"""Stereo should have roughly double the capacity of mono."""
|
|
from soosef.stegasoo.spread_steganography import calculate_audio_spread_capacity
|
|
|
|
mono_cap = calculate_audio_spread_capacity(carrier_wav_long, chip_tier=0)
|
|
stereo_cap = calculate_audio_spread_capacity(carrier_wav_stereo_long, chip_tier=0)
|
|
|
|
# Stereo should be ~1.5-2.2x mono (not exact because header is ch0 only
|
|
# and the files have slightly different durations/sample rates)
|
|
ratio = stereo_cap.usable_capacity_bytes / mono_cap.usable_capacity_bytes
|
|
assert ratio > 1.3, f"Stereo/mono capacity ratio too low: {ratio}"
|
|
|
|
def test_lfe_skip_5_1(self, carrier_wav_5_1):
|
|
"""LFE channel (index 3) should be unmodified in 6-channel audio."""
|
|
from soosef.stegasoo.spread_steganography import embed_in_audio_spread
|
|
|
|
payload = b"LFE skip test"
|
|
seed = b"\xEE" * 32
|
|
|
|
# Read original LFE channel
|
|
orig_samples, _ = sf.read(io.BytesIO(carrier_wav_5_1), dtype="float64", always_2d=True)
|
|
orig_lfe = orig_samples[:, 3].copy()
|
|
|
|
stego_bytes, stats = embed_in_audio_spread(
|
|
payload, carrier_wav_5_1, seed, chip_tier=0
|
|
)
|
|
assert stats.embeddable_channels == 5 # 6 channels - 1 LFE = 5
|
|
|
|
stego_samples, _ = sf.read(io.BytesIO(stego_bytes), dtype="float64", always_2d=True)
|
|
stego_lfe = stego_samples[:, 3]
|
|
|
|
# LFE channel should be completely unmodified
|
|
np.testing.assert_array_equal(orig_lfe, stego_lfe)
|
|
|
|
def test_lfe_skip_roundtrip(self, carrier_wav_5_1):
|
|
"""5.1 audio embed/extract roundtrip with LFE skipping."""
|
|
from soosef.stegasoo.spread_steganography import (
|
|
embed_in_audio_spread,
|
|
extract_from_audio_spread,
|
|
)
|
|
|
|
payload = b"5.1 surround test"
|
|
seed = b"\xEE" * 32
|
|
|
|
stego_bytes, stats = embed_in_audio_spread(
|
|
payload, carrier_wav_5_1, seed, chip_tier=0
|
|
)
|
|
assert stats.channels == 6
|
|
assert stats.embeddable_channels == 5
|
|
|
|
extracted = extract_from_audio_spread(stego_bytes, seed)
|
|
assert extracted is not None
|
|
assert extracted == payload
|
|
|
|
|
|
# =============================================================================
|
|
# HEADER V2 TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestHeaderV2:
|
|
"""Tests for v2 header construction and parsing."""
|
|
|
|
def test_header_v2_build_parse_roundtrip(self):
|
|
from soosef.stegasoo.spread_steganography import _build_header_v2, _parse_header
|
|
|
|
data_length = 12345
|
|
chip_tier = 1
|
|
num_ch = 2
|
|
lfe_skipped = False
|
|
|
|
header = _build_header_v2(data_length, chip_tier, num_ch, lfe_skipped)
|
|
assert len(header) == 20
|
|
|
|
magic_valid, version, length, tier, nch, lfe = _parse_header(header)
|
|
assert magic_valid
|
|
assert version == 2
|
|
assert length == data_length
|
|
assert tier == chip_tier
|
|
assert nch == num_ch
|
|
assert lfe is False
|
|
|
|
def test_header_v2_with_lfe_flag(self):
|
|
from soosef.stegasoo.spread_steganography import _build_header_v2, _parse_header
|
|
|
|
header = _build_header_v2(999, 0, 5, lfe_skipped=True)
|
|
magic_valid, version, length, tier, nch, lfe = _parse_header(header)
|
|
assert magic_valid
|
|
assert version == 2
|
|
assert length == 999
|
|
assert tier == 0
|
|
assert nch == 5
|
|
assert lfe is True
|
|
|
|
def test_header_v0_build_parse(self):
|
|
from soosef.stegasoo.spread_steganography import _build_header_v0, _parse_header
|
|
|
|
header = _build_header_v0(4567)
|
|
assert len(header) == 16
|
|
|
|
magic_valid, version, length, tier, nch, lfe = _parse_header(header)
|
|
assert magic_valid
|
|
assert version == 0
|
|
assert length == 4567
|
|
assert tier is None
|
|
assert nch is None
|
|
|
|
def test_header_bad_magic(self):
|
|
from soosef.stegasoo.spread_steganography import _parse_header
|
|
|
|
bad_header = b"XXXX" + b"\x00" * 16
|
|
magic_valid, version, length, tier, nch, lfe = _parse_header(bad_header)
|
|
assert not magic_valid
|
|
|
|
|
|
# =============================================================================
|
|
# ROUND-ROBIN BIT DISTRIBUTION TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestRoundRobin:
|
|
"""Tests for round-robin bit distribution."""
|
|
|
|
def test_distribute_and_collect_identity(self):
|
|
from soosef.stegasoo.spread_steganography import (
|
|
_collect_bits_round_robin,
|
|
_distribute_bits_round_robin,
|
|
)
|
|
|
|
bits = [1, 0, 1, 1, 0, 0, 1, 0, 1, 1]
|
|
for num_ch in [1, 2, 3, 4, 5]:
|
|
per_ch = _distribute_bits_round_robin(bits, num_ch)
|
|
assert len(per_ch) == num_ch
|
|
reassembled = _collect_bits_round_robin(per_ch)
|
|
assert reassembled == bits, f"Failed for {num_ch} channels"
|
|
|
|
def test_distribute_round_robin_ordering(self):
|
|
from soosef.stegasoo.spread_steganography import _distribute_bits_round_robin
|
|
|
|
bits = [0, 1, 2, 3, 4, 5] # using ints for clarity
|
|
per_ch = _distribute_bits_round_robin(bits, 3)
|
|
# ch0: bits 0, 3 ch1: bits 1, 4 ch2: bits 2, 5
|
|
assert per_ch[0] == [0, 3]
|
|
assert per_ch[1] == [1, 4]
|
|
assert per_ch[2] == [2, 5]
|
|
|
|
def test_distribute_uneven(self):
|
|
from soosef.stegasoo.spread_steganography import (
|
|
_collect_bits_round_robin,
|
|
_distribute_bits_round_robin,
|
|
)
|
|
|
|
bits = [0, 1, 2, 3, 4] # 5 bits across 3 channels
|
|
per_ch = _distribute_bits_round_robin(bits, 3)
|
|
assert per_ch[0] == [0, 3]
|
|
assert per_ch[1] == [1, 4]
|
|
assert per_ch[2] == [2]
|
|
|
|
reassembled = _collect_bits_round_robin(per_ch)
|
|
assert reassembled == bits
|
|
|
|
|
|
# =============================================================================
|
|
# CHANNEL MANAGEMENT TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestChannelManagement:
|
|
"""Tests for embeddable channel selection."""
|
|
|
|
def test_mono(self):
|
|
from soosef.stegasoo.spread_steganography import _embeddable_channels
|
|
|
|
assert _embeddable_channels(1) == [0]
|
|
|
|
def test_stereo(self):
|
|
from soosef.stegasoo.spread_steganography import _embeddable_channels
|
|
|
|
assert _embeddable_channels(2) == [0, 1]
|
|
|
|
def test_5_1_skips_lfe(self):
|
|
from soosef.stegasoo.spread_steganography import _embeddable_channels
|
|
|
|
channels = _embeddable_channels(6)
|
|
assert channels == [0, 1, 2, 4, 5]
|
|
assert 3 not in channels # LFE skipped
|
|
|
|
def test_7_1_skips_lfe(self):
|
|
from soosef.stegasoo.spread_steganography import _embeddable_channels
|
|
|
|
channels = _embeddable_channels(8)
|
|
assert 3 not in channels
|
|
assert len(channels) == 7
|
|
|
|
def test_quad_no_skip(self):
|
|
from soosef.stegasoo.spread_steganography import _embeddable_channels
|
|
|
|
# 4 channels < 6, so no LFE skip
|
|
assert _embeddable_channels(4) == [0, 1, 2, 3]
|
|
|
|
|
|
# =============================================================================
|
|
# FORMAT DETECTION TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestFormatDetection:
|
|
"""Tests for audio format detection."""
|
|
|
|
def test_detect_wav(self, carrier_wav):
|
|
from soosef.stegasoo.audio_utils import detect_audio_format
|
|
|
|
assert detect_audio_format(carrier_wav) == "wav"
|
|
|
|
def test_detect_unknown(self):
|
|
from soosef.stegasoo.audio_utils import detect_audio_format
|
|
|
|
assert detect_audio_format(b"not audio data") == "unknown"
|
|
|
|
def test_detect_empty(self):
|
|
from soosef.stegasoo.audio_utils import detect_audio_format
|
|
|
|
assert detect_audio_format(b"") == "unknown"
|
|
|
|
|
|
# =============================================================================
|
|
# AUDIO INFO TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestAudioInfo:
|
|
"""Tests for audio info extraction."""
|
|
|
|
def test_get_wav_info(self, carrier_wav):
|
|
from soosef.stegasoo.audio_utils import get_audio_info
|
|
|
|
info = get_audio_info(carrier_wav)
|
|
assert isinstance(info, AudioInfo)
|
|
assert info.sample_rate == 44100
|
|
assert info.channels == 1
|
|
assert info.format == "wav"
|
|
assert abs(info.duration_seconds - 1.0) < 0.1
|
|
|
|
def test_get_stereo_info(self, carrier_wav_stereo):
|
|
from soosef.stegasoo.audio_utils import get_audio_info
|
|
|
|
info = get_audio_info(carrier_wav_stereo)
|
|
assert info.channels == 2
|
|
|
|
|
|
# =============================================================================
|
|
# VALIDATION TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestAudioValidation:
|
|
"""Tests for audio validation."""
|
|
|
|
def test_validate_valid_audio(self, carrier_wav):
|
|
from soosef.stegasoo.audio_utils import validate_audio
|
|
|
|
result = validate_audio(carrier_wav)
|
|
assert result.is_valid
|
|
|
|
def test_validate_empty_audio(self):
|
|
from soosef.stegasoo.audio_utils import validate_audio
|
|
|
|
result = validate_audio(b"")
|
|
assert not result.is_valid
|
|
|
|
def test_validate_invalid_audio(self):
|
|
from soosef.stegasoo.audio_utils import validate_audio
|
|
|
|
result = validate_audio(b"not audio data at all")
|
|
assert not result.is_valid
|
|
|
|
def test_validate_audio_embed_mode(self):
|
|
from soosef.stegasoo.validation import validate_audio_embed_mode
|
|
|
|
assert validate_audio_embed_mode("audio_lsb").is_valid
|
|
assert validate_audio_embed_mode("audio_spread").is_valid
|
|
assert validate_audio_embed_mode("audio_auto").is_valid
|
|
assert not validate_audio_embed_mode("invalid").is_valid
|
|
|
|
|
|
# =============================================================================
|
|
# INTEGRATION TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestIntegration:
|
|
"""End-to-end integration tests using encode_audio/decode_audio."""
|
|
|
|
def test_lsb_encode_decode(self, carrier_wav, reference_photo):
|
|
from soosef.stegasoo.decode import decode_audio
|
|
from soosef.stegasoo.encode import encode_audio
|
|
|
|
stego_audio, stats = encode_audio(
|
|
message="Hello from audio steganography!",
|
|
reference_photo=reference_photo,
|
|
carrier_audio=carrier_wav,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_lsb",
|
|
)
|
|
|
|
assert len(stego_audio) > 0
|
|
|
|
result = decode_audio(
|
|
stego_audio=stego_audio,
|
|
reference_photo=reference_photo,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_lsb",
|
|
)
|
|
|
|
assert result.is_text
|
|
assert result.message == "Hello from audio steganography!"
|
|
|
|
def test_lsb_wrong_credentials(self, carrier_wav, reference_photo):
|
|
from soosef.stegasoo.decode import decode_audio
|
|
from soosef.stegasoo.encode import encode_audio
|
|
|
|
stego_audio, _ = encode_audio(
|
|
message="Secret",
|
|
reference_photo=reference_photo,
|
|
carrier_audio=carrier_wav,
|
|
passphrase="correct horse battery staple",
|
|
pin="123456",
|
|
embed_mode="audio_lsb",
|
|
)
|
|
|
|
with pytest.raises(Exception):
|
|
decode_audio(
|
|
stego_audio=stego_audio,
|
|
reference_photo=reference_photo,
|
|
passphrase="wrong passphrase words here",
|
|
pin="654321",
|
|
embed_mode="audio_lsb",
|
|
)
|
|
|
|
def test_spread_encode_decode(self, carrier_wav_spread_integration, reference_photo):
|
|
"""Test full spread spectrum encode/decode pipeline."""
|
|
from soosef.stegasoo.decode import decode_audio
|
|
from soosef.stegasoo.encode import encode_audio
|
|
|
|
stego_audio, stats = encode_audio(
|
|
message="Spread integration test",
|
|
reference_photo=reference_photo,
|
|
carrier_audio=carrier_wav_spread_integration,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_spread",
|
|
)
|
|
|
|
result = decode_audio(
|
|
stego_audio=stego_audio,
|
|
reference_photo=reference_photo,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_spread",
|
|
)
|
|
|
|
assert result.message == "Spread integration test"
|
|
|
|
def test_spread_encode_decode_with_chip_tier(
|
|
self, carrier_wav_spread_integration, reference_photo
|
|
):
|
|
"""Test spread spectrum with explicit chip tier."""
|
|
from soosef.stegasoo.decode import decode_audio
|
|
from soosef.stegasoo.encode import encode_audio
|
|
|
|
stego_audio, stats = encode_audio(
|
|
message="Tier 0 integration",
|
|
reference_photo=reference_photo,
|
|
carrier_audio=carrier_wav_spread_integration,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_spread",
|
|
chip_tier=0,
|
|
)
|
|
|
|
assert stats.chip_tier == 0
|
|
assert stats.chip_length == 256
|
|
|
|
result = decode_audio(
|
|
stego_audio=stego_audio,
|
|
reference_photo=reference_photo,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_spread",
|
|
)
|
|
|
|
assert result.message == "Tier 0 integration"
|
|
|
|
def test_auto_detect_lsb(self, carrier_wav, reference_photo):
|
|
"""Test auto-detection finds LSB encoded audio."""
|
|
from soosef.stegasoo.decode import decode_audio
|
|
from soosef.stegasoo.encode import encode_audio
|
|
|
|
stego_audio, _ = encode_audio(
|
|
message="Auto-detect test",
|
|
reference_photo=reference_photo,
|
|
carrier_audio=carrier_wav,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_lsb",
|
|
)
|
|
|
|
result = decode_audio(
|
|
stego_audio=stego_audio,
|
|
reference_photo=reference_photo,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_auto",
|
|
)
|
|
|
|
assert result.message == "Auto-detect test"
|
|
|
|
def test_spread_with_real_speech(self, speech_wav, reference_photo):
|
|
"""Test spread spectrum with real speech audio from test_data."""
|
|
from soosef.stegasoo.decode import decode_audio
|
|
from soosef.stegasoo.encode import encode_audio
|
|
|
|
message = "Hidden in a speech about elitism"
|
|
|
|
stego_audio, stats = encode_audio(
|
|
message=message,
|
|
reference_photo=reference_photo,
|
|
carrier_audio=speech_wav,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_spread",
|
|
chip_tier=0, # lossless tier for max capacity
|
|
)
|
|
|
|
assert stats.chip_tier == 0
|
|
|
|
result = decode_audio(
|
|
stego_audio=stego_audio,
|
|
reference_photo=reference_photo,
|
|
passphrase="test words here now",
|
|
pin="123456",
|
|
embed_mode="audio_spread",
|
|
)
|
|
|
|
assert result.message == message
|