Version: 1.2.0 (INTERNAL REVIEW — no external audit) Last Updated: 2026-02-24 Classification: Security-Critical Documentation
This document formally specifies the security invariants that Meow Decoder MUST uphold. These are mathematical properties that must hold for ALL inputs - violating any invariant is a security vulnerability.
Invariants are verified through:
- Property-based testing (Hypothesis) - Random input fuzzing
- Unit tests - Specific edge cases
- Fuzzing (AFL++) - Crash detection
- Code review - Manual verification
- Formal proofs (where available) - Machine-checked mathematical proofs
Last audited: 2026-02-25 — branch
main. Rows marked ✅ are CI-gated with real tool invocation on every push/PR. Rows marked⚠️ STRUCTURAL have proof blocks present in source but are NOT machine-checked by Z3 in CI — only structural regression tracking (proof fn / spec fn counts). Run Verus locally for full Z3 verification. Seeformal/Dockerfile.verusfor details.
| Property set | Tool | CI-gated? | Artefact |
|---|---|---|---|
| Guard-page memory safety — POSIX (GB-001–GB-008) | Verus | verus_guarded_buffer.rs |
|
| Guard-page memory safety — Windows VirtualProtect (WG-001–WG-007) | Verus | verus_windows_guard.rs |
|
| AEAD nonce uniqueness, auth-gated plaintext, key zeroization, no-bypass (AEAD-001–004) | Verus | aead_wrapper.rs |
|
| AEAD INT-CTXT, AAD binding, fail-closed, ratchet independence (AEAD-005–012) | Verus | aead_wrapper.rs |
|
| KDF parameter security, HKDF domain separation (KDF-001–004) | Verus | verus_kdf_proofs.rs |
|
| Shamir reconstruction constant-time (CT-001–004) | Verus | verus_kdf_proofs.rs |
|
| Protocol state machine (auth-then-output, replay, duress) | TLA+ | ✅ | MeowEncode.tla, MeowFountain.tla, MeowStreaming.tla |
| Ratchet index monotonicity, skip-key DoS, key uniqueness | TLA+ | ✅ | MeowRatchet.tla |
| Constant-time execution model (jitter-tolerant ≤1 · Jitter) | TLA+ | ✅ | TimingEqualizer.tla |
| Message expiry fail-closed | TLA+ | ✅ | ExpiryProtocol.tla |
| Cross-session forward secrecy (master ratchet) | TLA+ | ✅ | MasterRatchet.tla |
| Dolev-Yao secrecy & authentication | ProVerif | ✅ | meow_encode.pv |
| PQ beacon PCS restoration (ML-KEM-768 + X25519 hybrid) | ProVerif | ✅ | pq_beacon_pcs.pv |
| Manifest HMAC integrity | ProVerif | ✅ | manifest_signing.pv |
| Dead-man’s switch duress enforcement | ProVerif | ✅ | deadmans_switch_duress.pv |
| INV-004: 8-field canonical AAD binding | ProVerif | ✅ | meow_aad_8field_binding.pv |
| MEOW3 duress observational equivalence | Tamarin | ✅ | MeowDuressEquiv.spthy |
| MEOW4/5 PQ duress OE | Tamarin | ✅ | MeowDuressEquivPQ.spthy |
| AEAD 4-ary AAD binding | Tamarin | ✅ | MeowAEADBinding.spthy |
| Full Schrödinger deniability game (15 lemmas) | Tamarin | ✅ | MeowSchrodingerDeniability.spthy |
| Invisible-salamanders key-commitment prevention | Tamarin | ✅ | MeowKeyCommitment.spthy |
| Per-frame forward secrecy + PCS via beacon | Tamarin | ✅ | MeowRatchetFS.spthy |
| Guard-page overflow/underflow (symbolic) | Tamarin | ✅ | secure_alloc_guard_pages.spthy |
| Dead-man’s switch deadline (4 lemmas) | Tamarin | ✅ | meow_deadmans_switch.spthy |
| Header encryption OE (frame-index indistinguishability) | Tamarin | ✅ | MeowRatchetHeaderOE.spthy |
| Schrödinger OE (dual-password indistinguishability, simplified) | Tamarin | ✅ | MeowSchrodingerOE.spthy |
| Encoding observational equivalence (minimal) | Tamarin | ✅ | meow_encode_equiv.spthy |
| Fountain code LT-correctness (machine-checked) | Lean 4 | ✅ | FountainCodes.lean (1 approved sorry) |
| Shamir threshold security (field axiom) | Lean 4 | ✅ | ShamirSecretSharing.lean (1 approved axiom) |
| HKDF domain separation (10 theorems) | Lean 4 | ✅ | DomainSeparation.lean |
Note on Verus (
⚠️ STRUCTURAL rows): The CI jobformal-verification.yml→verusperforms grep-based regression tracking ofproof fn/spec fndeclarations — it does NOT invoke the Verus/Z3 SMT solver. Theverus!{}blocks in source are syntactically present but are only machine-checked when run locally with the Verus toolchain. To run full Z3 verification:cargo install verus && cd crypto_core && verus src/aead_wrapper.rsNote on AEAD-005–012: The Verus lemmas are abstract — preconditions directly subsume postconditions. They verify the spec is internally consistent but do not check that the
aes-gcmcrate implementation satisfies INT-CTXT. True implementation-level INT-CTXT proof requires a GF(2¹²⁸) GHASH model in Verus (open research; documented as approved limitation RL-1 informal-10x-audit.md).
∀ plaintext, password, keyfile:
decrypt(encrypt(plaintext, password, keyfile), password, keyfile) == plaintext
Description: Decryption must exactly recover the original plaintext for any valid inputs.
Verification:
tests/test_property_based.py::TestEncryptDecryptInvariants::test_aes_gcm_roundtrip_*tests/test_property_based.py::TestEncryptDecryptInvariants::test_file_encrypt_decrypt_roundtriptests/test_encode.py/tests/test_decode_gif.py(encode-decode roundtrip)
Failure Impact: Data corruption, unrecoverable files.
∀ ciphertext, key, nonce, aad:
IF tampered(ciphertext) OR tampered(aad) THEN
decrypt(ciphertext, key, nonce, aad) RAISES Exception
Description: AES-GCM MUST verify the authentication tag BEFORE releasing any plaintext. This prevents chosen-ciphertext attacks.
Implementation:
- AES-GCM authentication tag (16 bytes)
- Manifest HMAC-SHA256 (32 bytes)
- Per-frame MAC (8 bytes)
Verification:
tests/test_property_based.py::TestTamperDetection::test_ciphertext_tampering_detectedtests/test_property_based.py::TestTamperDetection::test_aad_tampering_detectedtests/test_security.py::TestTamperDetection
Failure Impact: Forgery attacks, accepting malicious data.
∀ encryptions e1, e2 with same key K:
IF e1 ≠ e2 THEN nonce(e1) ≠ nonce(e2)
Description: Nonce reuse with the same key completely breaks AES-GCM security. Each encryption MUST use a unique (salt, nonce) pair.
Implementation:
- Deterministic synthetic IV via HKDF-SHA-256 (primary mechanism):
nonce = HKDF-SHA-256( IKM = transfer_root_key, salt = frame_counter (u64 BE) || SHA-256(manifest), info = b"aes-gcm-nonce-v1", len = 12 ) - Collision resistance: requires HKDF-SHA-256 collision (computationally infeasible).
- Crash-safe: no persistent counter state needed across restarts.
- Multi-process safe: manifest hash unique per transfer session.
- Schrödinger isolation:
additional_contextbyte distinguishes sub-streams. - Per-process reuse guard:
NonceGeneratortracks usedframe_countervalues and raisesRuntimeErroron duplicate (defense-in-depth). - Rust nonce counter uses compare-and-swap (CAS) loop instead of
fetch_addto prevent u64 overflow wrap-around causing nonce reuse (aead_wrapper.rs,nonce.rs). - Legacy fallback: 96-bit random nonce + 128-bit salt (224 bits combined) retained for backward compatibility (non-ratchet, non-SIV paths).
Verification:
tests/security/test_nonce_uniqueness.py(23 tests: sequential, crash/restart, multi-thread, Schrödinger isolation, edge cases)tests/test_property_based.py::TestNonceUniqueness::test_nonces_never_repeattests/test_security.py::test_nonce_uniqueness
Failure Impact: Complete cipher break, XOR of plaintexts revealed.
∀ manifest M, ciphertext C encrypted with key K:
M is bound to C via AES-GCM AAD
Changing any field in M invalidates decryption
Description: The manifest (containing lengths, hashes, parameters) MUST be cryptographically bound to the ciphertext. This prevents substitution attacks.
AAD Contents:
# Canonical AAD construction (see canonical_aad.py)
aad = struct.pack('<QQ', orig_len, comp_len) # Lengths
aad += salt # Salt
aad += sha256 # Original hash
aad += MAGIC # Version
if ephemeral_public_key:
aad += ephemeral_public_key # FS key binding
if pq_ciphertext:
aad += pq_ciphertext # PQ ciphertext binding (MEOW4/MEOW5)Verification:
tests/test_invariants.py::TestSecurityInvariants::test_invariant_aad_modification_rejectedtests/test_security.py::TestTamperDetection::test_manifest_tamperingtests/test_crypto.py— deterministic construction, backward compat, roundtrip
Failure Impact: Length oracle attacks, version downgrade attacks.
backend == "rust"
Description: Core encryption path requires Rust backend for constant-time guarantees and memory zeroing. Auxiliary modules (PQ signing, ML-KEM beacons) must fail closed when secure backends (Rust, OQS, or certified pure-Python implementations) are unavailable. Insecure fallback stubs are permanently disabled in production.
Verification:
tests/test_crypto_backend.py::TestBackendAvailability::test_rust_backend_availabletests/test_security_hardening.py::test_insecure_mldsa_stubs_disabledtests/test_security_hardening.py::test_insecure_mlkem_stubs_disabledtests/conftest.pyenforces Rust backend availability
Failure Impact: Loss of constant-time guarantees and guaranteed Rust-side memory zeroing, or accidental insecure fallback behavior in production (security regression).
∀ password P, salt S, keyfile K:
derive_key(P, S, K) == derive_key(P, S, K) // Same inputs → same output
∀ P1 ≠ P2, S:
derive_key(P1, S) ≠ derive_key(P2, S) // Different passwords → different keys
∀ P, S1 ≠ S2:
derive_key(P, S1) ≠ derive_key(P, S2) // Different salts → different keys
Description: Key derivation must be deterministic (for decryption to work) but also ensure different inputs produce different keys.
Verification:
tests/test_property_based.py::TestKeyDerivationInvariants::*
Failure Impact: Decryption failure, key collision attacks.
∀ a, b with len(a) == len(b):
time(compare(a, b)) is independent of (a, b)
Description: All security-critical comparisons MUST execute in constant time to prevent timing attacks.
Implementation:
secrets.compare_digest()for password/MAC comparison- Rust
subtlecrate for constant-time ops - Timing equalization delays (1-5ms jitter)
Verification:
tests/test_property_based.py::TestConstantTimeInvariants::*tests/test_constant_time.py
Failure Impact: Timing attacks can leak password/key bits.
∀ manifest M:
unpack_manifest(pack_manifest(M)) == M
Description: Manifest serialization must be perfectly reversible with no data loss.
Verification:
tests/test_property_based.py::TestManifestInvariants::test_manifest_roundtrip
Failure Impact: Data corruption, decryption parameter loss.
IF manifest signature is present:
decoder MUST verify it and reject on failure
IF manifest signature is absent AND signing is enabled (default):
decoder MUST reject with ValueError (fail-closed)
IF manifest signature is absent AND signing is explicitly disabled
(MEOW_MANIFEST_SIGNING=off):
decoder MAY continue, but MUST emit a prominent
unsigned-manifest warning to stderr
Description: Signature verification is mandatory by default (fail-closed). Unsigned manifests are rejected unless the operator explicitly disables signing via MEOW_MANIFEST_SIGNING=off for legacy compatibility. When signing is disabled, unsigned manifests are accepted with an explicit stderr warning.
Critical Warning: Disabling signing (MEOW_MANIFEST_SIGNING=off) removes manifest forgery protection. Only use for legacy/transport-limited streams where re-encoding is not possible.
Verification:
tests/test_security_hardening.py::test_decoder_rejects_unsigned_manifest_when_signing_enabledtests/test_decode_gif.py::test_decode_gif_signed_manifest_verifiestests/test_decode_gif.py::test_decode_gif_tampered_signature_rejected
Failure Impact: Silent acceptance of forged metadata or unsafe operator assumptions in coercive/adversarial environments.
∀ data D, with k_blocks K:
IF received >= ceil(K * 1.05) droplets THEN
decode_probability > 0.99
Description: Fountain codes (Luby Transform) must allow recovery with approximately k blocks (with small overhead).
Verification:
tests/test_property_based.py::TestFountainCodeInvariants::test_fountain_roundtriptests/test_fountain.py
Failure Impact: Unrecoverable data despite sufficient frames.
∀ keypairs (a, A) and (b, B):
ECDH(a, B) == ECDH(b, A)
Description: X25519 key exchange must be commutative - both parties derive the same shared secret.
Verification:
tests/test_property_based.py::TestX25519Invariants::test_x25519_shared_secret_commutative
Failure Impact: Forward secrecy broken, key mismatch.
∀ private key sk, public key pk:
IF ECDH(sk, pk) == [0x00; 32] THEN RAISE Error
Description: X25519 exchange must reject all-zero shared secrets, which indicate small-subgroup attack.
The Rust backend (x25519_exchange() in pure.rs) performs constant-time all-zero check and returns Err.
Verification:
rust_crypto/tests/comprehensive_tests.rs(X25519 zero-check tests)rust_crypto/tests/additional_security_tests.rs
Failure Impact: Key agreement with attacker-controlled public key yields known shared secret.
∀ key K, message M, tag T = HMAC(K, M):
∀ M' ≠ M: HMAC_verify(K, M', T) == False
Description: HMAC verification must reject any modification to the authenticated message.
Verification:
tests/test_property_based.py::TestTamperDetection::test_hmac_tampering_detected
Failure Impact: Message forgery, manifest tampering.
∀ ciphertext C encrypted with password P:
∀ P' ≠ P: decrypt(C, P') RAISES Exception
Description: Decryption with an incorrect password MUST fail cleanly (not produce garbage).
Implementation:
- HMAC verification before decryption
- AES-GCM tag verification
- SHA-256 hash verification of decrypted data
Verification:
tests/test_encode.py/tests/test_decode_gif.py(wrong password rejection)tests/test_invariants.py::test_invariant_wrong_password_rejected
Failure Impact: Silent data corruption, oracle attacks.
∀ sensitive data S (passwords, keys):
AFTER use: S is overwritten with zeros
Description: Sensitive data should be zeroed after use to limit memory forensics exposure. Rust-side zeroing is guaranteed via the zeroize crate; Python-side zeroing (e.g., SecureBytes, ctypes.memset) is best-effort due to GC and allocator limitations.
Implementation:
SecureBytesclass with__del__zeroing (best-effort in Python)- Rust
zeroizecrate for guaranteed automatic zeroing mlock()to prevent swap (where available)
Verification:
tests/test_constant_time.py::test_secure_memory_zeroing
Failure Impact: Key recovery via memory forensics.
∀ passwords P (normal), D (duress):
time(check(P)) ≈ time(check(D))
Description: Checking for a duress password must not reveal whether the password was the duress password through timing.
Implementation:
- Constant-time comparison with
secrets.compare_digest() - Timing equalization with random delays
Verification:
tests/test_duress_mode.py::test_duress_timing
Failure Impact: Attacker can distinguish duress from real password.
∀ frame F with MAC M computed with key K, salt S, index I:
IF modified(F) OR modified(M) THEN
verify_frame_mac(F, M, K, S, I) == False
Description: Per-frame MACs prevent injection of malicious frames into the QR stream.
Verification:
tests/test_frame_mac.pytests/test_adversarial.py::test_frame_injection
Failure Impact: DoS via malicious frame injection.
∀ stego encryptions e1, e2:
nonce(e1) ≠ nonce(e2)
Description: Each multi-layer steganography encryption MUST use a fresh 96-bit random nonce. Nonce reuse with AES-GCM completely breaks confidentiality (XOR of plaintexts revealed).
Implementation:
os.urandom(12)generated per encryption call- Nonce prepended to ciphertext for transmission
- Previously: hardcoded zero-nonce (FIXED 2026-02-20)
Verification:
tests/test_stego_adversarial.py::TestNonceUniquenesstests/test_stego_fuzz.py(Hypothesis roundtrip tests)
Failure Impact: Complete cipher break — XOR of plaintexts revealed.
∀ stego operations:
IF crypto_backend_unavailable THEN
RAISE RuntimeError (never silently skip encryption)
Description: Multi-layer steganography MUST raise an exception if the cryptographic backend is unavailable. Previously logged a warning and continued with unencrypted data.
Implementation:
raise RuntimeErrorwhen no crypto backend available- Previously:
logger.warning()(fail-open, FIXED 2026-02-20)
Verification:
tests/test_stego_adversarial.py::TestFailClosedEncryption
Failure Impact: Plaintext embedded without encryption — total confidentiality loss.
∀ message M, cover C, shared_key K:
stc_decode(stc_encode(M, C, K), K) == M
Description: Syndrome-Trellis Codes MUST correctly roundtrip: encoding a message into a cover signal and decoding it back MUST recover the original message exactly.
Implementation:
- Viterbi trellis algorithm with checkpoint-based backtracking (Rust), rate 1/4
- Previously: GF(2) Gaussian elimination (FIXED 2026-02-20), then replaced with Viterbi (Session 3)
Verification:
tests/test_stego_adversarial.py::TestSTCCorrectnesstests/test_stego_fuzz.py::test_stc_roundtrip(Hypothesis)
Failure Impact: Data corruption — embedded payload unrecoverable.
∀ payload P, capacity C:
IF len(P) > C THEN
RAISE ValueError (never silently truncate or warn-only)
Description: When the payload exceeds the steganographic capacity of the cover image, the system MUST reject the operation with an exception. Silent truncation or warning-only causes data loss.
Implementation:
raise ValueErrorwhen payload exceeds capacity- Previously:
logger.warning()(FIXED 2026-02-20)
Verification:
tests/test_stego_adversarial.py::TestCapacityEnforcement
Failure Impact: Silent data truncation — receiver gets corrupted/incomplete payload.
∀ payload P, cover frames F[]:
IF format == APNG (lossless) THEN
LSB_extract(LSB_embed(F[], P)) == P
IF format == GIF (palette-quantized) THEN
LSB_extract(LSB_embed(F[], P)) ≠ P (NOT guaranteed)
Description: Steganographic LSB embedding MUST use a lossless image format (APNG) to preserve pixel values. GIF uses 256-color palette quantization which corrupts LSB-embedded data beyond recovery. Cat mode MUST output APNG (.png), never GIF (.gif).
Implementation:
- Cat mode encoder outputs APNG via
apng.APNG(lossless animated PNG) - GIF path reserved for non-stego (QR-only) output
- Stego extraction fallback in
decode_gif.pytries LSB depths 2, 1, 3
Verification:
web_demo/test_cat_mode.py— APNG cat mode encode→decode roundtripweb_demo/test_all_modes.py— Cat APNG × 5 runs (100% pass rate)tests/test_stego_phase1.py::TestTemporalChannel::test_embed_extract_roundtrip_*
Failure Impact: Complete data loss — QR frames unrecoverable from GIF-quantized stego images.
∀ stego-encoded animation A with N total frames:
LET extracted[] = stego_extract(A) // may skip frames
LET qr_frame_indices[] = original frame positions of extracted QR codes
∀ i in extracted[]:
verify_frame_mac(extracted[i], key, salt, qr_frame_indices[i]) == True
Description: When decoding stego-encoded animations, not all frames yield readable QR codes. Frame MAC verification MUST use the original frame index (position in the animation), not the sequential index in the extracted list. Using sequential indices causes MAC failures for frames after the first skipped frame.
Implementation:
decode_gif.pytracksqr_frame_indices[]during both normal QR scan and stego extraction- Frame MAC verification uses
actual_frame_idx = qr_frame_indices[idx + 1]instead ofidx + 1
Verification:
web_demo/test_all_modes.py— Cat mode with MAC verification (100% MAC success rate)web_demo/test_cat_mode.py— Single roundtrip with MAC check
Failure Impact: False MAC rejection — valid frames rejected when stego extraction skips frames, causing decode failure despite correct data.
| Invariant | Property Tests | Unit Tests | Fuzzing | Status |
|---|---|---|---|---|
| INV-001 | ✅ | ✅ | ✅ | VERIFIED |
| INV-002 | ✅ | ✅ | ✅ | VERIFIED |
| INV-003 | ✅ | ✅ | - | VERIFIED |
| INV-004 | ✅ | ✅ | ✅ | VERIFIED |
| INV-005 | ✅ | ✅ | - | VERIFIED |
| INV-006 | ✅ | ✅ | - | VERIFIED |
| INV-007 | ✅ | ✅ | - | PARTIAL* |
| INV-008 | ✅ | ✅ | ✅ | VERIFIED |
| INV-009 | ✅ | ✅ | ✅ | VERIFIED |
| INV-010 | ✅ | ✅ | - | VERIFIED |
| INV-011 | ✅ | ✅ | - | VERIFIED |
| INV-012 | ✅ | ✅ | - | VERIFIED |
| INV-013 | - | ✅ | - | PARTIAL* |
| INV-014 | - | ✅ | - | PARTIAL* |
| INV-015 | - | ✅ | - | VERIFIED |
| INV-016 | ✅ | ✅ | - | VERIFIED |
| INV-017 | - | ✅ | - | VERIFIED |
| INV-018 | ✅ | ✅ | - | VERIFIED |
| INV-019 | - | ✅ | - | VERIFIED |
| INV-020 | - | ✅ | - | VERIFIED |
| INV-021 | - | ✅ | - | VERIFIED |
∀ chain positions i < j:
Given chain_key[j], it is computationally infeasible to derive chain_key[i]
(HKDF-SHA256 is one-way)
Description: Compromising the ratchet state at frame N reveals nothing about frames 0..N-1. Each chain_key is zeroized immediately after deriving its successor. There is no API, state transition, or code path that allows recovering a previous chain key.
Implementation:
ratchet_step()deriveschain_key[i+1]then dropschain_key[i]handle- Skip cache stores only
message_keyhandles (not chain keys) - Rust
zeroizecrate guarantees handle memory is wiped on drop
Verification:
tests/security/test_ratchet_forward_secrecy.py::TestForwardSecrecytests/test_ratchet.py::TestRatchetForwardSecrecy
Failure Impact: Loss of forward secrecy — past messages exposed on chain compromise.
∀ ratchet-encrypted frames F:
IF aad_mismatch(F) OR sequence_invalid(F) THEN
abort_entire_decode() // no partial plaintext
Description: Any AAD mismatch, sequence number violation, or key commitment failure during ratchet decryption MUST abort the entire decode operation. No partial plaintext is ever emitted.
Verification:
tests/security/test_ratchet_forward_secrecy.py::TestRatchetFailClosedtests/test_ratchet.py::TestRatchetReplay
Failure Impact: Partial plaintext leak under active attack.
∀ ratchet states S at position P:
¬∃ operation that produces state S' at position P' < P
Description: The ratchet state machine has no backward transition. Once a chain key is consumed, it cannot be re-derived. The consumed-set prevents re-processing the same frame index.
Verification:
tests/security/test_ratchet_forward_secrecy.py::TestNoRollback
Failure Impact: Replay attacks, forward secrecy violation.
⚠️ Schrödinger mode provides LIMITED deniability:
- Casual inspection: two valid decryptions exist (plausible deniability)
- Nation-state forensic analysis: statistical distinguishability
MAY be detectable via timing, file size patterns, entropy analysis,
or comparison of multiple files from the same user
- This is NOT perfect cryptographic deniability against unlimited
compute and multiple samples
Description: Schrödinger mode is designed so that both sub-streams (real + decoy/dummy) are always present and each password reveals only its own stream. However, advanced forensic analysis MAY detect dual encoding. Users in high-risk environments should not rely on deniability alone.
What IS guaranteed:
- Each password independently decrypts only its sub-stream
- Both sub-streams always present (even in "single secret" mode)
- Independent Argon2id, ratchet, fountain, and GCM keys per stream
- No cross-commitments between streams
What is NOT guaranteed (with mitigations noted):
- Perfect indistinguishability under forensic comparison of multiple files (mitigated by INV-030 fixed-size padding + INV-031 fixed QR +
decorrelation.py— best-effort) Resistance to timing side-channels during encode/decode→ Mitigated by INV-029timing_equalizer.py— best-effort, Python GC is non-constant-timeDeniability if attacker has access to swap/memory forensics→ Mitigated by INV-026memory_guard.py(mlockall + MADV_DONTDUMP) — best-effortDeniability if attacker compares file sizes across users→ Mitigated by INV-030size_normalizer.py(fixed size classes)
Note (2026-02-22): The above mitigations significantly raise the bar but are best-effort. Python's GC, interpreter scheduling, and OS-level page cache remain outside software control.
Verification:
tests/security/test_deniability.py(statistical distinguishability tests)tests/security/test_timing_equalizer.py,test_size_normalizer.py,test_memory_guard.py
Failure Impact: False sense of security for users in rogue states.
*PARTIAL indicates implementation is best-effort due to Python limitations.
Status: ✅ ENFORCED
Category: Memory Hardening
Implemented In: meow_decoder/memory_guard.py (274 lines)
Description: At process start, activate_memory_guard() enforces:
mlockall(MCL_CURRENT | MCL_FUTURE)— prevent all pages from swapRLIMIT_CORE = 0— prevent core dump generationPR_SET_DUMPABLE = 0— prevent ptrace attachmentMADV_DONTDUMPon sensitive memory regions
What IS guaranteed:
- Best-effort prevention of key material in swap and core dumps (Linux/macOS)
- Process is non-dumpable and non-ptraceable
What is NOT guaranteed:
- Protection on Windows (deferred)
- Protection against root-level memory access or cold boot attacks
Verification: tests/security/test_memory_guard.py, tests/security/test_dontdump.py
Failure Impact: Key material in swap/core dumps recoverable by forensic examiner.
Status: ✅ ENFORCED
Category: Forensic Countermeasures
Implemented In: meow_decoder/secure_temp.py (265 lines)
Description: All temporary file operations use tmpfs-backed storage (/dev/shm preferred). Falls back to /tmp with a SecurityWarning if no tmpfs is available.
Verification: tests/security/test_secure_temp.py
Failure Impact: Temp files persisted to disk, recoverable by forensic examiner.
Status: ✅ ENFORCED (best-effort)
Category: Forensic Countermeasures
Implemented In: meow_decoder/forensic_cleanup.py (387 lines)
Description: On graceful exit, ForensicCleaner.clean_all() removes:
- File manager thumbnails (GNOME, KDE, macOS QuickLook, Windows)
- Recent file lists (
recently-used.xbel) - Clipboard contents (xclip/pbcopy)
- Shell history entries containing meow-related commands
- Temp files matching
meow_*
Limitation: Best-effort and OS-dependent. Cannot clean kernel page cache or filesystem journal from userspace.
Verification: tests/security/test_forensic_cleanup.py
Failure Impact: OS artifacts reveal file operation history to forensic examiner.
Status: ✅ ENFORCED (best-effort)
Category: Timing Side-Channels
Implemented In: meow_decoder/timing_equalizer.py (281 lines)
Description: TimingEqualizer wraps decode operations to produce constant wall-clock time regardless of success/failure. Uses CSPRNG jitter (±5%) to prevent statistical averaging.
Limitation: Python GC and interpreter scheduling are inherently non-constant-time. Rust-side crypto uses subtle crate for true constant-time.
Verification: tests/security/test_timing_equalizer.py
Failure Impact: Timing oracle reveals password validity.
Status: ✅ ENFORCED
Category: Indistinguishability
Implemented In: meow_decoder/size_normalizer.py (288 lines)
Description: GIF output is padded to fixed size classes (4KB, 16KB, 64KB, 256KB, 1MB, 4MB, 16MB, 64MB) to prevent file size fingerprinting.
Verification: tests/security/test_size_normalizer.py
Failure Impact: File size reveals payload size, enabling profiling.
Status: ✅ ENFORCED
Category: Indistinguishability
Implemented In: meow_decoder/qr_code.py, meow_decoder/config.py
Description: QR version is fixed at v25 regardless of payload size. Prevents QR structure from leaking payload size metadata.
Verification: Config-level enforcement; QR version not auto-selected. Failure Impact: QR version metadata reveals payload size class.
Status: ✅ ENFORCED
Category: Anti-Forensics
Implemented In: meow_decoder/expiry.py (332 lines)
Description: Encoded content can include an expiry timestamp. On decode, expiry is checked BEFORE decryption. Expired content triggers self-destruct (multi-pass overwrite + unlink) rather than silent decryption.
Verification: tests/security/test_expiry.py
Failure Impact: Expired secrets remain accessible indefinitely.
Status: ✅ ENFORCED
Category: Memory Safety
Implemented In: meow_decoder/memory_guard.py
∀ GuardedBuffer(size):
write(data, offset) with offset + len(data) > size → RAISES ValueError
read(length, offset) with offset + length > size → RAISES ValueError
close() → zero(buffer) ∧ munmap(pages)
close(); close() → NO CRASH (idempotent)
close(); write(data) → RAISES RuntimeError
Description: GuardedBuffer allocates memory with PROT_NONE guard pages above and below the data region. Any out-of-bounds access traps to a signal/exception. Double-free and use-after-free are handled safely. Buffer contents are zeroed on close to prevent forensic recovery.
Verification:
tests/test_fuzz_coverage_integration.py::TestGuardedBuffer(9 tests)fuzz/fuzz_windows_guard.py(8 fuzz functions)
Failure Impact: Sensitive key material leaked via buffer overread; use-after-free produces undefined behavior.
Status: ✅ ENFORCED
Category: Authentication
Implemented In: meow_decoder/secure_keyboard.py::MouseGesturePassword
∀ points, grid_size:
quantize(points) is deterministic
∀ p₁, p₂ in same grid cell: collect(p₁) == collect(p₂)
∀ p₁, p₂ in different cells: collect(p₁) ≠ collect(p₂) (collision-resistant)
BLAKE2b(quantized, person=b"meow_gesture_v1", digest_size=32)
Description: Mouse gesture authentication quantizes continuous (x,y) coordinates to a discrete grid (default 16×16). Points within the same cell produce identical keys. The BLAKE2b person tag b"meow_gesture_v1" provides domain separation, preventing cross-protocol key reuse. Output is a 32-byte (64 hex char) key.
Verification:
tests/test_fuzz_coverage_integration.py::TestMouseGesturePassword(8 tests)fuzz/fuzz_mouse_gesture.py(10 fuzz functions)
Failure Impact: Gesture key instability causes authentication failures; missing domain separation enables cross-protocol attacks.
Status: ✅ ENFORCED
Category: Integrity
Implemented In: meow_decoder/tamper_detection.py
∀ TamperState s:
from_bytes(to_bytes(s)).baseline_hashes == s.baseline_hashes
from_bytes(corrupt(to_bytes(s))) == None (HMAC reject)
from_bytes(truncate(to_bytes(s))) == None (HMAC reject)
∀ seed: silent_poison_bytes(n, seed) == silent_poison_bytes(n, seed)
∀ seed₁ ≠ seed₂: silent_poison_bytes(n, seed₁) ≠ silent_poison_bytes(n, seed₂)
Description: TamperState serialization includes an HMAC-SHA256 authentication tag computed from the state key. Deserialization verifies the HMAC before returning the state; any corruption or truncation causes rejection (returns None). silent_poison_bytes provides deterministic-per-seed random bytes for poisoning tampered outputs without revealing the tamper detection mechanism.
Verification:
tests/test_fuzz_coverage_integration.py::TestTamperState(4 tests)tests/test_fuzz_coverage_integration.py::TestSilentPoison(3 tests)tests/test_fuzz_coverage_integration.py::TestTamperDetector(2 tests)fuzz/fuzz_tamper_detection.py(10 fuzz functions)
Failure Impact: Attacker modifies checkpoint to disable tamper detection; poisoned output leaks real data.
Status: ✅ ENFORCED
Category: Steganography
Implemented In: meow_decoder/adversarial_carrier.py, meow_decoder/stego_advanced.py
∀ seed, width, height:
sensor(seed) ≠ texture(seed) ≠ dct(seed) ≠ combined(seed)
∀ algo: algo(seed₁) == algo(seed₁) (deterministic)
∀ algo, seed₁ ≠ seed₂: algo(seed₁) ≠ algo(seed₂) (non-degenerate)
rotation_schedule = ["sensor", "texture", "dct", "combined"]
∀ i: frame_algo = schedule[i % 4]
carrier_noise ∈ [-128, 127] ∧ integer
Description: The adversarial steganography rotation schedule cycles through four noise algorithms (sensor, texture, DCT, combined) to defeat steganalysis detectors. Each algorithm must produce distinct statistical fingerprints for the same seed. The schedule is deterministic and covers all four algorithms within every 4 frames. Carrier noise is integer-valued within signed byte range.
Verification:
tests/test_fuzz_coverage_integration.py::TestAdversarialNoiseGenerator(9 tests)fuzz/fuzz_adversarial_stego.py(10 fuzz functions, differential)
Failure Impact: Steganalysis detector trains on single noise pattern; rotation bypass enables carrier detection.
Status: ✅ FORMALLY VERIFIED (Tamarin)
Category: Deniability
Implemented In: meow_decoder/quantum_mixer.py, meow_decoder/schrodinger_encode.py
Formal Model: formal/tamarin/MeowSchrodingerDeniability.spthy
∀ pass_A, pass_B, payload_A, payload_B:
QuantumNoise = XOR(KDF(pass_A), KDF(pass_B))
Stream_A = Enc(pass_A, payload_A ⊕ QuantumNoise)
Stream_B = Enc(pass_B, payload_B ⊕ QuantumNoise)
Deniability: Observe(Stream_A) reveals nothing about payload_B (and vice versa)
Coercion Safety: revealing pass_A reveals only payload_A
KDF Commitment: commit(KDF(pass)) is binding
No Cross-Leak: decode(pass_A) never outputs payload_B
Description: Schrödinger mode encodes two independent secrets into a single GIF using quantum plausible deniability. The XOR of password-derived KDF outputs creates a noise layer that is computationally indistinguishable from random without both passwords. A coerced user revealing one password cannot be proven to possess the second secret. Formalized as observational equivalence in Tamarin with 10 lemmas including deniability, integrity, no-cross-leak, coercion safety, and KDF commitment binding.
Verification:
formal/tamarin/MeowSchrodingerDeniability.spthy(10 lemmas)formal/tamarin/MeowSchrodingerOE.spthy(6 lemmas, observational equivalence)tests/test_fuzz_coverage_integration.py::TestSchrodingerStructure(3 tests)fuzz/fuzz_schrodinger.py(existing fuzz target)
Failure Impact: Adversary distinguishes which secret was accessed; deniability claim is false.
Status: ✅ FUZZ-TESTED
Category: Memory Safety
Implemented In: meow_decoder/memory_guard.py
Fuzz Target: fuzz/fuzz_windows_guard.py (11 functions)
∀ alloc_free_cycles:
rapid_alloc_free(N cycles) → no leak, no crash
boundary_write(buf, size) succeeds; boundary_write(buf, size+1) raises
zero_wipe(buf)^N is idempotent: read(buf) = 0x00 * size
Description: GuardedBuffer must survive rapid allocation/deallocation cycles without memory leaks, correctly enforce write boundaries, and produce consistently zeroed state after multiple wipe operations. Fuzz targets exercise guard page traps, VirtualLock/VirtualProtect lifecycle, double-free safety, use-after-free detection, concurrent alloc/free, and boundary conditions.
Verification:
fuzz/fuzz_windows_guard.py(11 fuzz functions)tests/test_fuzz_coverage_integration.py::TestGuardedBuffer(9 tests)tests/test_fuzz_coverage_integration.py::TestGuardedBufferEnhanced(4 tests)
Failure Impact: Memory corruption, information leakage via unwiped buffers, or denial of service.
Status: ✅ FUZZ-TESTED
Category: Authentication
Implemented In: meow_decoder/secure_keyboard.py
Fuzz Target: fuzz/fuzz_mouse_gesture.py (13 functions)
∀ gesture_points, grid_size:
BLAKE2b(gesture, person=b"meow_gesture_v1\x00") ≠ BLAKE2b(gesture)
quantize(points) is deterministic for fixed grid_size
∀ i ≠ j: gesture_i ≠ gesture_j → key_i ≠ key_j (collision resistance)
replay_resistance: H(gesture || salt_1) ≠ H(gesture || salt_2) for salt_1 ≠ salt_2
Description: Mouse gesture authentication derives keys via BLAKE2b with a mandatory person tag for domain separation. Quantization must be deterministic across grid sizes, person tag must affect output, and different gestures must produce different keys. Fuzz targets exercise quantization extremes (zero/huge/negative coords), perturbation stability, collision resistance, person tag consistency, replay resistance via salts, and grid boundary behavior.
Verification:
fuzz/fuzz_mouse_gesture.py(13 fuzz functions)tests/test_fuzz_coverage_integration.py::TestMouseGesturePassword(8 tests)tests/test_fuzz_coverage_integration.py::TestMouseGestureEnhanced(5 tests)
Failure Impact: Auth bypass via gesture collision, domain confusion with other BLAKE2b uses, or replay attacks.
Status: ✅ FUZZ-TESTED
Category: Tamper Resistance
Implemented In: meow_decoder/tamper_detection.py
Fuzz Target: fuzz/fuzz_tamper_detection.py (13 functions)
∀ state, data:
TamperState.deserialize(corrupt(serialize(state))) raises ValueError
silent_poison(seed, n) has Shannon entropy > 5.0 bits/byte
silent_poison(seed_1, n) ≠ silent_poison(seed_2, n) for seed_1 ≠ seed_2
checkpoint_hmac(state) is binding: flip(checkpoint) → rejection
state_version_corruption → rejection
Description: TamperState serialization must reject corrupted data (bit flips, truncation, version corruption). Silent poison output must be high-entropy and seed-dependent. Checkpoint HMAC integrity must be fail-closed. Fuzz targets exercise state roundtrip, corruption variants, poison determinism/uniqueness/randomness/entropy, detector with fake modules, poison output length, checkpoint HMAC integrity, baseline reinit, version field corruption, and checkpoint replay scenarios.
Verification:
fuzz/fuzz_tamper_detection.py(13 fuzz functions)tests/test_fuzz_coverage_integration.py::TestTamperState(4 tests)tests/test_fuzz_coverage_integration.py::TestSilentPoison(3 tests)tests/test_fuzz_coverage_integration.py::TestTamperDetector(2 tests)tests/test_fuzz_coverage_integration.py::TestTamperDetectionEnhanced(5 tests)
Failure Impact: Undetected code tampering, low-entropy poison output enabling timing attacks, or checkpoint rollback attacks.
Status: ✅ FUZZ-TESTED
Category: Steganography
Implemented In: meow_decoder/adversarial_carrier.py
Fuzz Target: fuzz/fuzz_adversarial_stego.py (13 functions)
∀ seed_1 ≠ seed_2:
noise(seed_1) ≠ noise(seed_2)
|correlation(noise(seed_1), noise(seed_2))| < 0.5
∀ method ∈ {sensor, texture, dct, combined}:
noise_method(seed) is deterministic
all values are finite (no NaN/Inf)
∀ frames(100+):
rotation_schedule visits ≥ 3 of 4 algorithms
Description: Adversarial stego noise generators must produce statistically independent outputs for different seeds, with low cross-correlation. All noise values must be finite. The rotation schedule must visit all algorithm variants over sufficient frames to prevent single-pattern detection. Fuzz targets exercise rotation differential, per-algorithm determinism, histogram equalization, DCT matching PSNR, combined component completeness, carrier noise range, pairs test, rotation schedule coverage, noise profile extremes, rotation frequency distribution, statistical uniformity, and cross-seed independence.
Verification:
fuzz/fuzz_adversarial_stego.py(13 fuzz functions)tests/test_fuzz_coverage_integration.py::TestAdversarialNoiseGenerator(9 tests)tests/test_fuzz_coverage_integration.py::TestAdversarialStegoEnhanced(5 tests)
Failure Impact: Steganalysis detector identifies noise pattern; carrier detection compromises steganographic concealment.
Status: ✅ TESTED + FORMALLY MODELED
Category: Forward Secrecy / Post-Quantum
Implemented In: meow_decoder/ratchet.py
Formal Model: formal/tamarin/MeowSchrodingerDeniability.spthy (15 lemmas)
DEFAULT_REKEY_INTERVAL > 0 (asymmetric rekey enabled by default)
_generate_kem_beacon / _recover_kem_beacon: REMOVED (dead code cleanup)
Active PQ path: _generate_asym_rekey / _recover_asym_rekey (MSR v2.0)
PQ_BEACON_MIX_INFO ≠ REKEY_BEACON_INFO ≠ REKEY_BEACON_KEM_INFO (domain separation)
HEADER_ENC_INFO ≠ HEADER_MASK_INFO ≠ REKEY_BEACON_INFO (domain separation)
Tamarin lemmas 11-15: RatchetForwardSecrecy, PQBeaconDomainSeparation,
MultiSessionUnlinkability, AsymRekeyPCS, HeaderEncryptionConfidentiality
Description: The MSR v2.0 ratchet provides per-frame forward secrecy via HKDF chain ratcheting and periodic asymmetric rekey (X25519 ECDH + optional ML-KEM-1024). Legacy dead-code KEM beacon functions were removed; active PQ integration uses _generate_asym_rekey/_recover_asym_rekey. DEFAULT_REKEY_INTERVAL is set to 32 (enabled by default). Tamarin model extended with 5 new lemmas covering ratchet forward secrecy, PQ beacon domain separation, multi-session unlinkability, asymmetric rekey PCS, and header encryption confidentiality.
Verification:
formal/tamarin/MeowSchrodingerDeniability.spthy(15 lemmas)tests/test_fuzz_coverage_integration.py::TestRatchetIntegration(5 tests)tests/test_ratchet.py(existing ratchet tests)tests/test_asymmetric_rekey.py(asymmetric rekey tests)
Failure Impact: Compromise of chain_key yields past/future frame keys; PQ beacon provides no quantum resistance; traffic analysis via unencrypted frame indices.
Status: ✅ TESTED
Category: Correctness / Thread Safety
Implemented In: meow_decoder/fountain.py
∀ concurrent calls to FountainEncoder.droplet(seed):
output(seed) is deterministic AND independent of global PRNG state
Description: FountainEncoder.droplet() now uses a local random.Random(seed) instance
instead of the global random module, ensuring thread safety and reproducible output
regardless of concurrent access. sample_degree() accepts an optional rng parameter.
Verification:
tests/test_fountain.py(deterministic droplet generation tests)tests/test_e2e_crypto_fountain.py(end-to-end pipeline)
Failure Impact: Non-deterministic fountain encoding; intermittent decode failures under concurrent use.
Status: ✅ TESTED
Category: Cryptographic Safety
Implemented In: rust_crypto/src/handles.rs
∀ HKDF handle calls: output_len == 32 OR RAISE Error
Description: All 6 HKDF handle functions in the Rust backend enforce output_len == 32.
This prevents callers from accidentally requesting non-standard key lengths that could
weaken security or cause protocol mismatches.
Verification:
rust_crypto/tests/comprehensive_tests.rs(HKDF output length tests)
Failure Impact: Callers could request shorter keys, weakening cipher security.
When adding a new security-critical feature:
- Document the invariant in this file
- Add property-based tests in
tests/test_property_based.py - Add targeted unit tests in appropriate test file
- Add fuzz target in
fuzz/if parsing is involved - Update the test matrix above
# Run all property-based tests
pytest tests/test_property_based.py -v --hypothesis-show-statistics
# Run invariant tests only
pytest tests/test_invariants.py -v
# Run fuzz coverage integration tests (INV-033 through INV-042)
pytest tests/test_fuzz_coverage_integration.py -v
# Run full security test suite
pytest tests/test_security.py tests/test_invariants.py tests/test_property_based.py -v
# Run with coverage for crypto paths
pytest --cov=meow_decoder.crypto --cov=meow_decoder.crypto_backend \
--cov-report=html --cov-fail-under=90
# Run fuzzing (new targets)
python -m atheris fuzz/fuzz_manifest.py
python fuzz/fuzz_windows_guard.py # Guard page memory safety
python fuzz/fuzz_mouse_gesture.py # Gesture auth quantization
python fuzz/fuzz_tamper_detection.py # Tamper detection + poisoning
python fuzz/fuzz_adversarial_stego.py # Stego rotation differential- AES-GCM Nonce Reuse Attack
- Argon2 OWASP Guidelines
- Timing Attacks on Web Applications
- NIST SP 800-63B Password Guidelines
Security Contact: Open a GitHub issue with [SECURITY] tag