Rebrand SooSeF to FieldWitness

Complete project rebrand for better positioning in the press freedom
and digital security space. FieldWitness communicates both field
deployment and evidence testimony — appropriate for the target audience
of journalists, NGOs, and human rights organizations.

Rename mapping:
- soosef → fieldwitness (package, CLI, all imports)
- soosef.stegasoo → fieldwitness.stego
- soosef.verisoo → fieldwitness.attest
- ~/.soosef/ → ~/.fwmetadata/ (innocuous data dir name)
- SOOSEF_DATA_DIR → FIELDWITNESS_DATA_DIR
- SoosefConfig → FieldWitnessConfig
- SoosefError → FieldWitnessError

Also includes:
- License switch from MIT to GPL-3.0
- C2PA bridge module (Phase 0-2 MVP): cert.py, export.py, vendor_assertions.py
- README repositioned to lead with provenance/federation, stego backgrounded
- Threat model skeleton at docs/security/threat-model.md
- Planning docs: docs/planning/c2pa-integration.md, docs/planning/gtm-feasibility.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-04-02 15:05:13 -04:00
parent 6325e86873
commit 490f9d4a1d
188 changed files with 4588 additions and 2017 deletions

View File

@@ -1,4 +1,4 @@
"""Shared test fixtures for SooSeF tests."""
"""Shared test fixtures for FieldWitness tests."""
from __future__ import annotations
@@ -9,16 +9,16 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
@pytest.fixture()
def tmp_soosef_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Set SOOSEF_DATA_DIR to a temporary directory.
def tmp_fieldwitness_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Set FIELDWITNESS_DATA_DIR to a temporary directory.
This must be used before importing any module that reads soosef.paths
This must be used before importing any module that reads fieldwitness.paths
at import time. For modules that read paths lazily (most of them),
monkeypatching the paths module directly is more reliable.
"""
data_dir = tmp_path / ".soosef"
data_dir = tmp_path / ".fieldwitness"
data_dir.mkdir()
monkeypatch.setenv("SOOSEF_DATA_DIR", str(data_dir))
monkeypatch.setenv("FIELDWITNESS_DATA_DIR", str(data_dir))
return data_dir

View File

@@ -5,7 +5,7 @@ from io import BytesIO
import pytest
from PIL import Image
from soosef.verisoo.hashing import hash_image, perceptual_distance, is_same_image
from fieldwitness.attest.hashing import hash_image, perceptual_distance, is_same_image
def create_test_image(width: int = 100, height: int = 100, color: tuple = (255, 0, 0)) -> bytes:

View File

@@ -11,10 +11,10 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PublicKey,
)
from soosef.exceptions import ChainError, ChainIntegrityError
from soosef.federation.chain import ChainStore
from soosef.federation.models import ChainState
from soosef.federation.serialization import canonical_bytes, compute_record_hash
from fieldwitness.exceptions import ChainError, ChainIntegrityError
from fieldwitness.federation.chain import ChainStore
from fieldwitness.federation.models import ChainState
from fieldwitness.federation.serialization import canonical_bytes, compute_record_hash
def test_genesis_record(chain_dir: Path, private_key: Ed25519PrivateKey):
@@ -236,10 +236,10 @@ def test_verify_chain_detects_signer_change(chain_dir: Path):
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from uuid_utils import uuid7
from soosef.federation.entropy import collect_entropy_witnesses
from soosef.federation.models import AttestationChainRecord
from soosef.federation.serialization import canonical_bytes as cb
from soosef.federation.serialization import serialize_record
from fieldwitness.federation.entropy import collect_entropy_witnesses
from fieldwitness.federation.models import AttestationChainRecord
from fieldwitness.federation.serialization import canonical_bytes as cb
from fieldwitness.federation.serialization import serialize_record
state = store.state()
prev_hash = state.head_hash
@@ -289,7 +289,7 @@ def test_verify_chain_detects_signer_change(chain_dir: Path):
def test_key_rotation_in_chain(chain_dir: Path):
"""Chain with a proper key rotation record verifies successfully."""
from soosef.federation.chain import CONTENT_TYPE_KEY_ROTATION
from fieldwitness.federation.chain import CONTENT_TYPE_KEY_ROTATION
store = ChainStore(chain_dir)
key1 = Ed25519PrivateKey.generate()

View File

@@ -14,8 +14,8 @@ from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from soosef.exceptions import ChainError
from soosef.federation.chain import MAX_RECORD_SIZE, ChainStore
from fieldwitness.exceptions import ChainError
from fieldwitness.federation.chain import MAX_RECORD_SIZE, ChainStore
def test_concurrent_append_no_fork(chain_dir: Path):

View File

@@ -26,11 +26,11 @@ from click.testing import CliRunner
@pytest.fixture()
def soosef_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect soosef paths to a tmp directory."""
import soosef.paths as paths
def fieldwitness_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect fieldwitness paths to a tmp directory."""
import fieldwitness.paths as paths
data_dir = tmp_path / ".soosef"
data_dir = tmp_path / ".fieldwitness"
data_dir.mkdir()
monkeypatch.setattr(paths, "BASE_DIR", data_dir)
return data_dir
@@ -59,8 +59,8 @@ def _write_deadman_state(
def test_enforcement_loop_no_op_when_disarmed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Loop should not call check() when the switch is not armed."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
# Redirect the module-level DEADMAN_STATE constant so DeadmanSwitch() default is our tmp file
state_file = tmp_path / "deadman.json"
@@ -90,8 +90,8 @@ def test_enforcement_loop_no_op_when_disarmed(tmp_path: Path, monkeypatch: pytes
def test_enforcement_loop_fires_when_overdue(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Loop must call DeadmanSwitch.check() when armed and past interval + grace."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))
@@ -120,8 +120,8 @@ def test_enforcement_loop_fires_when_overdue(tmp_path: Path, monkeypatch: pytest
def test_enforcement_loop_exits_after_firing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""After firing, the loop must return and not call check() a second time."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))
@@ -145,8 +145,8 @@ def test_enforcement_loop_exits_after_firing(tmp_path: Path, monkeypatch: pytest
def test_enforcement_loop_tolerates_exceptions(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Transient errors in check() must not kill the loop."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))
@@ -183,8 +183,8 @@ def test_enforcement_loop_tolerates_exceptions(tmp_path: Path, monkeypatch: pyte
def test_start_deadman_thread_is_daemon(monkeypatch: pytest.MonkeyPatch):
"""Thread must be a daemon so it dies with the process."""
# Patch the loop to exit immediately so the thread doesn't hang in tests
import soosef.cli as cli_mod
from soosef.cli import _start_deadman_thread
import fieldwitness.cli as cli_mod
from fieldwitness.cli import _start_deadman_thread
monkeypatch.setattr(cli_mod, "_deadman_enforcement_loop", lambda interval_seconds: None)
@@ -207,10 +207,10 @@ def test_check_deadman_disarmed(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 0 and prints helpful message when not armed."""
from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
# Point at an empty tmp dir so the real ~/.soosef/fieldkit/deadman.json isn't read
# Point at an empty tmp dir so the real ~/.fieldwitness/fieldkit/deadman.json isn't read
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))
@@ -223,8 +223,8 @@ def test_check_deadman_armed_ok(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 0 when armed and check-in is current."""
from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))
@@ -247,8 +247,8 @@ def test_check_deadman_overdue_in_grace(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 0 but prints OVERDUE warning when past interval but in grace."""
from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))
@@ -273,8 +273,8 @@ def test_check_deadman_fires_when_expired(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 2 when the switch has fully expired."""
from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
from fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"DEADMAN_STATE": state_file}))

View File

@@ -7,11 +7,11 @@ from pathlib import Path
import pytest
from click.testing import CliRunner
import soosef.paths as _paths
from soosef.cli import main
from soosef.exceptions import KeystoreError
from soosef.keystore.manager import KeystoreManager
from soosef.keystore.models import RotationResult
import fieldwitness.paths as _paths
from fieldwitness.cli import main
from fieldwitness.exceptions import KeystoreError
from fieldwitness.keystore.manager import KeystoreManager
from fieldwitness.keystore.models import RotationResult
# ---------------------------------------------------------------------------
# Helpers
@@ -21,7 +21,7 @@ from soosef.keystore.models import RotationResult
def _make_manager(tmp_path: Path) -> KeystoreManager:
"""Return a KeystoreManager pointing at isolated temp directories."""
identity_dir = tmp_path / "identity"
channel_key_file = tmp_path / "stegasoo" / "channel.key"
channel_key_file = tmp_path / "stego" / "channel.key"
return KeystoreManager(identity_dir=identity_dir, channel_key_file=channel_key_file)
@@ -144,7 +144,7 @@ class TestRotateChannelKey:
ks.rotate_channel_key()
def test_raises_for_env_var_key(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STEGASOO_CHANNEL_KEY", "a" * 64)
monkeypatch.setenv("FIELDWITNESS_CHANNEL_KEY", "a" * 64)
ks = _make_manager(tmp_path)
# has_channel_key() returns True (env), but file doesn't exist
with pytest.raises(KeystoreError, match="environment variable"):
@@ -182,7 +182,7 @@ class TestRotateChannelKey:
assert oct(key_file.stat().st_mode & 0o777) == oct(0o600)
def test_archived_key_matches_old_fingerprint(self, tmp_path: Path):
from soosef.stegasoo.crypto import get_channel_fingerprint
from fieldwitness.stego.crypto import get_channel_fingerprint
ks = _make_manager(tmp_path)
ks.generate_channel_key()
@@ -197,7 +197,7 @@ class TestRotateChannelKey:
assert old_fp == result.old_fingerprint
def test_new_channel_key_active_after_rotation(self, tmp_path: Path):
from soosef.stegasoo.crypto import get_channel_fingerprint
from fieldwitness.stego.crypto import get_channel_fingerprint
ks = _make_manager(tmp_path)
ks.generate_channel_key()
@@ -226,14 +226,14 @@ class TestRotateChannelKey:
class TestRotateCLI:
def _init_soosef(self, tmp_path: Path) -> Path:
def _init_fieldwitness(self, tmp_path: Path) -> Path:
"""Create the minimal directory + key structure for CLI tests.
Temporarily sets paths.BASE_DIR so the lazy-resolved KeystoreManager
writes keys to the same location the CLI will read from when invoked
with --data-dir pointing at the same directory.
"""
data_dir = tmp_path / ".soosef"
data_dir = tmp_path / ".fieldwitness"
original_base = _paths.BASE_DIR
try:
_paths.BASE_DIR = data_dir
@@ -245,7 +245,7 @@ class TestRotateCLI:
return data_dir
def test_rotate_identity_cli_success(self, tmp_path: Path):
data_dir = self._init_soosef(tmp_path)
data_dir = self._init_fieldwitness(tmp_path)
runner = CliRunner()
result = runner.invoke(
@@ -261,7 +261,7 @@ class TestRotateCLI:
assert "IMPORTANT:" in result.output
def test_rotate_identity_cli_no_identity(self, tmp_path: Path):
data_dir = tmp_path / ".soosef"
data_dir = tmp_path / ".fieldwitness"
data_dir.mkdir()
runner = CliRunner()
@@ -274,7 +274,7 @@ class TestRotateCLI:
assert "Error" in result.output
def test_rotate_channel_cli_success(self, tmp_path: Path):
data_dir = self._init_soosef(tmp_path)
data_dir = self._init_fieldwitness(tmp_path)
runner = CliRunner()
result = runner.invoke(
@@ -290,7 +290,7 @@ class TestRotateCLI:
assert "IMPORTANT:" in result.output
def test_rotate_channel_cli_no_key(self, tmp_path: Path):
data_dir = tmp_path / ".soosef"
data_dir = tmp_path / ".fieldwitness"
data_dir.mkdir()
runner = CliRunner()
@@ -303,7 +303,7 @@ class TestRotateCLI:
assert "Error" in result.output
def test_rotate_identity_aborts_without_confirmation(self, tmp_path: Path):
data_dir = self._init_soosef(tmp_path)
data_dir = self._init_fieldwitness(tmp_path)
runner = CliRunner()
# Simulate the user typing "n" at the confirmation prompt
@@ -316,7 +316,7 @@ class TestRotateCLI:
assert result.exit_code != 0
def test_rotate_channel_aborts_without_confirmation(self, tmp_path: Path):
data_dir = self._init_soosef(tmp_path)
data_dir = self._init_fieldwitness(tmp_path)
runner = CliRunner()
result = runner.invoke(

View File

@@ -16,11 +16,11 @@ from cryptography.hazmat.primitives.serialization import (
@pytest.fixture()
def populated_soosef(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Create a populated ~/.soosef directory with identity, chain, attestations, etc."""
import soosef.paths as paths
def populated_fieldwitness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Create a populated ~/.fieldwitness directory with identity, chain, attestations, etc."""
import fieldwitness.paths as paths
data_dir = tmp_path / ".soosef"
data_dir = tmp_path / ".fieldwitness"
data_dir.mkdir()
monkeypatch.setattr(paths, "BASE_DIR", data_dir)
@@ -34,12 +34,12 @@ def populated_soosef(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
(identity_dir / "public.pem").write_bytes(pub_pem)
# Create channel key
stegasoo_dir = data_dir / "stegasoo"
stegasoo_dir.mkdir()
(stegasoo_dir / "channel.key").write_text("test-channel-key")
stego_dir = data_dir / "stego"
stego_dir.mkdir()
(stego_dir / "channel.key").write_text("test-channel-key")
# Create chain data
from soosef.federation.chain import ChainStore
from fieldwitness.federation.chain import ChainStore
chain_dir = data_dir / "chain"
store = ChainStore(chain_dir)
@@ -53,7 +53,7 @@ def populated_soosef(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
# Create other dirs
(data_dir / "auth").mkdir()
(data_dir / "auth" / "soosef.db").write_bytes(b"dummy db")
(data_dir / "auth" / "fieldwitness.db").write_bytes(b"dummy db")
(data_dir / "temp").mkdir()
(data_dir / "temp" / "file.tmp").write_bytes(b"tmp")
(data_dir / "instance").mkdir()
@@ -63,11 +63,11 @@ def populated_soosef(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
return data_dir
def test_purge_all_destroys_chain_data(populated_soosef: Path):
def test_purge_all_destroys_chain_data(populated_fieldwitness: Path):
"""CRITICAL: execute_purge(ALL) must destroy chain directory."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
chain_dir = populated_soosef / "chain"
chain_dir = populated_fieldwitness / "chain"
assert chain_dir.exists()
assert (chain_dir / "chain.bin").exists()
@@ -77,46 +77,46 @@ def test_purge_all_destroys_chain_data(populated_soosef: Path):
assert "destroy_chain_data" in result.steps_completed
def test_purge_all_destroys_identity(populated_soosef: Path):
def test_purge_all_destroys_identity(populated_fieldwitness: Path):
"""execute_purge(ALL) must destroy identity keys."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
assert (populated_soosef / "identity" / "private.pem").exists()
assert (populated_fieldwitness / "identity" / "private.pem").exists()
result = execute_purge(PurgeScope.ALL, reason="test")
assert not (populated_soosef / "identity").exists()
assert not (populated_fieldwitness / "identity").exists()
assert "destroy_identity_keys" in result.steps_completed
def test_purge_all_destroys_attestation_log(populated_soosef: Path):
"""execute_purge(ALL) must destroy the Verisoo attestation log."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
def test_purge_all_destroys_attestation_log(populated_fieldwitness: Path):
"""execute_purge(ALL) must destroy the Attest attestation log."""
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
result = execute_purge(PurgeScope.ALL, reason="test")
assert not (populated_soosef / "attestations").exists()
assert not (populated_fieldwitness / "attestations").exists()
assert "destroy_attestation_log" in result.steps_completed
def test_purge_keys_only_preserves_chain(populated_soosef: Path):
def test_purge_keys_only_preserves_chain(populated_fieldwitness: Path):
"""KEYS_ONLY purge destroys keys but preserves chain and attestation data."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
result = execute_purge(PurgeScope.KEYS_ONLY, reason="test")
# Keys gone
assert not (populated_soosef / "identity").exists()
assert not (populated_fieldwitness / "identity").exists()
assert "destroy_identity_keys" in result.steps_completed
# Chain and attestations preserved (KEYS_ONLY doesn't touch data)
assert (populated_soosef / "chain" / "chain.bin").exists()
assert (populated_soosef / "attestations" / "log.bin").exists()
assert (populated_fieldwitness / "chain" / "chain.bin").exists()
assert (populated_fieldwitness / "attestations" / "log.bin").exists()
def test_purge_reports_all_steps(populated_soosef: Path):
def test_purge_reports_all_steps(populated_fieldwitness: Path):
"""execute_purge(ALL) reports all expected steps including chain."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
result = execute_purge(PurgeScope.ALL, reason="test")

View File

@@ -4,8 +4,8 @@ from __future__ import annotations
import hashlib
from soosef.federation.models import AttestationChainRecord, ChainState, EntropyWitnesses
from soosef.federation.serialization import (
from fieldwitness.federation.models import AttestationChainRecord, ChainState, EntropyWitnesses
from fieldwitness.federation.serialization import (
canonical_bytes,
compute_record_hash,
deserialize_record,

View File

@@ -1,5 +1,5 @@
"""
Stegasoo Library Unit Tests
Stego Library Unit Tests
Tests core functionality: encode/decode, LSB/DCT modes, channel keys, validation.
"""
@@ -10,8 +10,8 @@ from pathlib import Path
import pytest
from PIL import Image
import soosef.stegasoo as stegasoo
from soosef.stegasoo import (
import fieldwitness.stego as stego
from fieldwitness.stego import (
decode,
decode_text,
encode,
@@ -33,7 +33,7 @@ REF_PATH = TEST_DATA / "ref.jpg"
# Test credentials
TEST_PASSPHRASE = "tower booty sunny windy toasty spicy"
TEST_PIN = "727643678"
TEST_MESSAGE = "Hello, Stegasoo!"
TEST_MESSAGE = "Hello, Stego!"
@pytest.fixture
@@ -61,11 +61,11 @@ class TestVersion:
"""Test version info."""
def test_version_exists(self):
assert hasattr(stegasoo, "__version__")
assert stegasoo.__version__
assert hasattr(stego, "__version__")
assert stego.__version__
def test_version_format(self):
parts = stegasoo.__version__.split(".")
parts = stego.__version__.split(".")
assert len(parts) >= 2
assert all(p.isdigit() for p in parts[:2])
@@ -76,7 +76,7 @@ class TestGeneration:
def test_generate_passphrase_default(self):
passphrase = generate_passphrase()
words = passphrase.split()
assert len(words) == stegasoo.DEFAULT_PASSPHRASE_WORDS
assert len(words) == stego.DEFAULT_PASSPHRASE_WORDS
def test_generate_passphrase_custom_length(self):
passphrase = generate_passphrase(words=8)
@@ -389,7 +389,7 @@ class TestEdgeCases:
def test_unicode_message(self, carrier_bytes, ref_bytes):
"""Test encoding Unicode messages."""
unicode_msg = "Hello 🦖 Stegasoo! 日本語 émojis"
unicode_msg = "Hello 🦖 Stego! 日本語 émojis"
result = encode(
message=unicode_msg,
@@ -521,17 +521,17 @@ class TestVideoSupport:
def test_video_support_flag_exists(self):
"""HAS_VIDEO_SUPPORT flag should exist."""
assert hasattr(stegasoo, "HAS_VIDEO_SUPPORT")
assert isinstance(stegasoo.HAS_VIDEO_SUPPORT, bool)
assert hasattr(stego, "HAS_VIDEO_SUPPORT")
assert isinstance(stego.HAS_VIDEO_SUPPORT, bool)
def test_video_constants_exist(self):
"""Video-related constants should exist."""
assert hasattr(stegasoo, "EMBED_MODE_VIDEO_LSB")
assert hasattr(stegasoo, "EMBED_MODE_VIDEO_AUTO")
assert hasattr(stego, "EMBED_MODE_VIDEO_LSB")
assert hasattr(stego, "EMBED_MODE_VIDEO_AUTO")
@pytest.mark.skipif(
not stegasoo.HAS_VIDEO_SUPPORT,
not stego.HAS_VIDEO_SUPPORT,
reason="Video support not available (ffmpeg or dependencies missing)",
)
class TestVideoFormatDetection:
@@ -542,21 +542,21 @@ class TestVideoFormatDetection:
if test_video_bytes is None:
pytest.skip("Could not create test video")
from soosef.stegasoo import detect_video_format
from fieldwitness.stego import detect_video_format
fmt = detect_video_format(test_video_bytes)
assert fmt in ("mp4", "mov")
def test_detect_video_format_unknown(self):
"""Should return 'unknown' for non-video data."""
from soosef.stegasoo import detect_video_format
from fieldwitness.stego import detect_video_format
fmt = detect_video_format(b"not a video")
assert fmt == "unknown"
@pytest.mark.skipif(
not stegasoo.HAS_VIDEO_SUPPORT,
not stego.HAS_VIDEO_SUPPORT,
reason="Video support not available (ffmpeg or dependencies missing)",
)
class TestVideoInfo:
@@ -567,7 +567,7 @@ class TestVideoInfo:
if test_video_bytes is None:
pytest.skip("Could not create test video")
from soosef.stegasoo import get_video_info
from fieldwitness.stego import get_video_info
info = get_video_info(test_video_bytes)
@@ -583,7 +583,7 @@ class TestVideoInfo:
if test_video_bytes is None:
pytest.skip("Could not create test video")
from soosef.stegasoo import validate_video
from fieldwitness.stego import validate_video
result = validate_video(test_video_bytes, check_duration=False)
@@ -592,7 +592,7 @@ class TestVideoInfo:
@pytest.mark.skipif(
not stegasoo.HAS_VIDEO_SUPPORT,
not stego.HAS_VIDEO_SUPPORT,
reason="Video support not available (ffmpeg or dependencies missing)",
)
class TestVideoCapacity:
@@ -603,7 +603,7 @@ class TestVideoCapacity:
if test_video_bytes is None:
pytest.skip("Could not create test video")
from soosef.stegasoo import calculate_video_capacity
from fieldwitness.stego import calculate_video_capacity
capacity_info = calculate_video_capacity(test_video_bytes)
@@ -615,7 +615,7 @@ class TestVideoCapacity:
@pytest.mark.skipif(
not stegasoo.HAS_VIDEO_SUPPORT,
not stego.HAS_VIDEO_SUPPORT,
reason="Video support not available (ffmpeg or dependencies missing)",
)
class TestVideoEncodeDecode:
@@ -626,7 +626,7 @@ class TestVideoEncodeDecode:
if test_video_bytes is None:
pytest.skip("Could not create test video")
from soosef.stegasoo import decode_video, encode_video
from fieldwitness.stego import decode_video, encode_video
message = "Secret video message!"
@@ -660,7 +660,7 @@ class TestVideoEncodeDecode:
if test_video_bytes is None:
pytest.skip("Could not create test video")
from soosef.stegasoo import decode_video, encode_video
from fieldwitness.stego import decode_video, encode_video
message = "Secret video message!"

View File

@@ -1,5 +1,5 @@
"""
Tests for Stegasoo audio steganography.
Tests for Stego audio steganography.
Tests cover:
- Audio LSB roundtrip (encode + decode)
@@ -23,10 +23,10 @@ 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
from fieldwitness.stego.constants import AUDIO_ENABLED, EMBED_MODE_AUDIO_LSB, EMBED_MODE_AUDIO_SPREAD
from fieldwitness.stego.models import AudioCapacityInfo, AudioEmbedStats, AudioInfo
pytestmark = pytest.mark.skipif(not AUDIO_ENABLED, reason="Audio support disabled (STEGASOO_AUDIO)")
pytestmark = pytest.mark.skipif(not AUDIO_ENABLED, reason="Audio support disabled (FIELDWITNESS_AUDIO)")
# Path to real test data files
_TEST_DATA = Path(__file__).parent.parent / "test_data"
@@ -177,7 +177,7 @@ class TestAudioLSB:
"""Tests for audio LSB steganography."""
def test_calculate_capacity(self, carrier_wav):
from soosef.stegasoo.audio_steganography import calculate_audio_lsb_capacity
from fieldwitness.stego.audio_steganography import calculate_audio_lsb_capacity
capacity = calculate_audio_lsb_capacity(carrier_wav)
assert capacity > 0
@@ -186,7 +186,7 @@ class TestAudioLSB:
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
from fieldwitness.stego.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
payload = b"Hello, audio steganography!"
key = b"\x42" * 32
@@ -205,7 +205,7 @@ class TestAudioLSB:
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
from fieldwitness.stego.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
payload = b"Stereo test message"
key = b"\xAB" * 32
@@ -218,7 +218,7 @@ class TestAudioLSB:
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
from fieldwitness.stego.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
payload = b"Secret message"
correct_key = b"\x42" * 32
@@ -231,7 +231,7 @@ class TestAudioLSB:
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
from fieldwitness.stego.audio_steganography import embed_in_audio_lsb, extract_from_audio_lsb
payload = b"Two bits per sample test"
key = b"\x55" * 32
@@ -243,7 +243,7 @@ class TestAudioLSB:
def test_generate_sample_indices(self):
"""Test deterministic sample index generation."""
from soosef.stegasoo.audio_steganography import generate_sample_indices
from fieldwitness.stego.audio_steganography import generate_sample_indices
key = b"\x42" * 32
indices1 = generate_sample_indices(key, 10000, 100)
@@ -263,7 +263,7 @@ 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
from fieldwitness.stego.spread_steganography import calculate_audio_spread_capacity
capacity = calculate_audio_spread_capacity(carrier_wav_long)
assert isinstance(capacity, AudioCapacityInfo)
@@ -274,7 +274,7 @@ class TestAudioSpread:
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
from fieldwitness.stego.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)
@@ -290,7 +290,7 @@ class TestAudioSpread:
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 (
from fieldwitness.stego.spread_steganography import (
embed_in_audio_spread,
extract_from_audio_spread,
)
@@ -311,7 +311,7 @@ class TestAudioSpread:
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 (
from fieldwitness.stego.spread_steganography import (
embed_in_audio_spread,
extract_from_audio_spread,
)
@@ -329,7 +329,7 @@ class TestAudioSpread:
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 (
from fieldwitness.stego.spread_steganography import (
embed_in_audio_spread,
extract_from_audio_spread,
)
@@ -347,7 +347,7 @@ class TestAudioSpread:
def test_wrong_seed_fails(self, carrier_wav_long):
"""Test that wrong seed produces no valid extraction."""
from soosef.stegasoo.spread_steganography import (
from fieldwitness.stego.spread_steganography import (
embed_in_audio_spread,
extract_from_audio_spread,
)
@@ -363,7 +363,7 @@ class TestAudioSpread:
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 (
from fieldwitness.stego.spread_steganography import (
embed_in_audio_spread,
extract_from_audio_spread,
)
@@ -387,7 +387,7 @@ class TestAudioSpread:
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
from fieldwitness.stego.spread_steganography import embed_in_audio_spread
payload = b"Spatial preservation test"
seed = b"\xCD" * 32
@@ -415,7 +415,7 @@ class TestAudioSpread:
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
from fieldwitness.stego.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)
@@ -427,7 +427,7 @@ class TestAudioSpread:
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
from fieldwitness.stego.spread_steganography import embed_in_audio_spread
payload = b"LFE skip test"
seed = b"\xEE" * 32
@@ -449,7 +449,7 @@ class TestAudioSpread:
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 (
from fieldwitness.stego.spread_steganography import (
embed_in_audio_spread,
extract_from_audio_spread,
)
@@ -477,7 +477,7 @@ 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
from fieldwitness.stego.spread_steganography import _build_header_v2, _parse_header
data_length = 12345
chip_tier = 1
@@ -496,7 +496,7 @@ class TestHeaderV2:
assert lfe is False
def test_header_v2_with_lfe_flag(self):
from soosef.stegasoo.spread_steganography import _build_header_v2, _parse_header
from fieldwitness.stego.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)
@@ -508,7 +508,7 @@ class TestHeaderV2:
assert lfe is True
def test_header_v0_build_parse(self):
from soosef.stegasoo.spread_steganography import _build_header_v0, _parse_header
from fieldwitness.stego.spread_steganography import _build_header_v0, _parse_header
header = _build_header_v0(4567)
assert len(header) == 16
@@ -521,7 +521,7 @@ class TestHeaderV2:
assert nch is None
def test_header_bad_magic(self):
from soosef.stegasoo.spread_steganography import _parse_header
from fieldwitness.stego.spread_steganography import _parse_header
bad_header = b"XXXX" + b"\x00" * 16
magic_valid, version, length, tier, nch, lfe = _parse_header(bad_header)
@@ -537,7 +537,7 @@ class TestRoundRobin:
"""Tests for round-robin bit distribution."""
def test_distribute_and_collect_identity(self):
from soosef.stegasoo.spread_steganography import (
from fieldwitness.stego.spread_steganography import (
_collect_bits_round_robin,
_distribute_bits_round_robin,
)
@@ -550,7 +550,7 @@ class TestRoundRobin:
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
from fieldwitness.stego.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)
@@ -560,7 +560,7 @@ class TestRoundRobin:
assert per_ch[2] == [2, 5]
def test_distribute_uneven(self):
from soosef.stegasoo.spread_steganography import (
from fieldwitness.stego.spread_steganography import (
_collect_bits_round_robin,
_distribute_bits_round_robin,
)
@@ -584,31 +584,31 @@ class TestChannelManagement:
"""Tests for embeddable channel selection."""
def test_mono(self):
from soosef.stegasoo.spread_steganography import _embeddable_channels
from fieldwitness.stego.spread_steganography import _embeddable_channels
assert _embeddable_channels(1) == [0]
def test_stereo(self):
from soosef.stegasoo.spread_steganography import _embeddable_channels
from fieldwitness.stego.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
from fieldwitness.stego.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
from fieldwitness.stego.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
from fieldwitness.stego.spread_steganography import _embeddable_channels
# 4 channels < 6, so no LFE skip
assert _embeddable_channels(4) == [0, 1, 2, 3]
@@ -623,17 +623,17 @@ class TestFormatDetection:
"""Tests for audio format detection."""
def test_detect_wav(self, carrier_wav):
from soosef.stegasoo.audio_utils import detect_audio_format
from fieldwitness.stego.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
from fieldwitness.stego.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
from fieldwitness.stego.audio_utils import detect_audio_format
assert detect_audio_format(b"") == "unknown"
@@ -647,7 +647,7 @@ class TestAudioInfo:
"""Tests for audio info extraction."""
def test_get_wav_info(self, carrier_wav):
from soosef.stegasoo.audio_utils import get_audio_info
from fieldwitness.stego.audio_utils import get_audio_info
info = get_audio_info(carrier_wav)
assert isinstance(info, AudioInfo)
@@ -657,7 +657,7 @@ class TestAudioInfo:
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
from fieldwitness.stego.audio_utils import get_audio_info
info = get_audio_info(carrier_wav_stereo)
assert info.channels == 2
@@ -672,25 +672,25 @@ class TestAudioValidation:
"""Tests for audio validation."""
def test_validate_valid_audio(self, carrier_wav):
from soosef.stegasoo.audio_utils import validate_audio
from fieldwitness.stego.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
from fieldwitness.stego.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
from fieldwitness.stego.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
from fieldwitness.stego.validation import validate_audio_embed_mode
assert validate_audio_embed_mode("audio_lsb").is_valid
assert validate_audio_embed_mode("audio_spread").is_valid
@@ -707,8 +707,8 @@ 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
from fieldwitness.stego.decode import decode_audio
from fieldwitness.stego.encode import encode_audio
stego_audio, stats = encode_audio(
message="Hello from audio steganography!",
@@ -733,8 +733,8 @@ class TestIntegration:
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
from fieldwitness.stego.decode import decode_audio
from fieldwitness.stego.encode import encode_audio
stego_audio, _ = encode_audio(
message="Secret",
@@ -756,8 +756,8 @@ class TestIntegration:
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
from fieldwitness.stego.decode import decode_audio
from fieldwitness.stego.encode import encode_audio
stego_audio, stats = encode_audio(
message="Spread integration test",
@@ -782,8 +782,8 @@ class TestIntegration:
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
from fieldwitness.stego.decode import decode_audio
from fieldwitness.stego.encode import encode_audio
stego_audio, stats = encode_audio(
message="Tier 0 integration",
@@ -810,8 +810,8 @@ class TestIntegration:
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
from fieldwitness.stego.decode import decode_audio
from fieldwitness.stego.encode import encode_audio
stego_audio, _ = encode_audio(
message="Auto-detect test",
@@ -834,8 +834,8 @@ class TestIntegration:
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
from fieldwitness.stego.decode import decode_audio
from fieldwitness.stego.encode import encode_audio
message = "Hidden in a speech about elitism"