Independent Security Audit β Meow Decoder Date: 2026-02-25 Auditor scope: Full codebase review (main branch, commit post-remediation) Methodology: Static source review + automated test verification; no runtime penetration testing
- Status: Fully Implemented
- Production path: Yes β
SecureBox<T>incrypto_core/src/secure_alloc.rsis used by the Rust crypto backend called from both encode and decode paths. - Reachability: production
- Evidence:
- POSIX:
crypto_core/src/secure_alloc.rsL105-189 βmmapPROT_NONE guard pages,mprotectdata,mlock,MADV_DONTDUMP - Windows:
crypto_core/src/secure_alloc.rsL213-296 βVirtualAllocPAGE_NOACCESS,VirtualProtectPAGE_READWRITE,VirtualLock,VirtualFree,VirtualUnlock - Zeroization on Drop:
crypto_core/src/secure_alloc.rsL302-304 βT::zeroize()+munmap/VirtualFree
- POSIX:
- Remaining weakness:
- Observed (Medium):
mlockfailure is silent in release builds (secure_alloc.rsL159-166). Keys may reside on swappable memory without caller awareness.is_locked()accessor exists but no production caller checks it. - Observed (Low): macOS lacks
MADV_DONTDUMPequivalent β key material appears in core dumps on macOS. - Fixed this session (Low):
sysconf(_SC_PAGESIZE)return value was cast tousizewithout checking for -1 error. Now fails closed withMmapFailed(0).
- Observed (Medium):
- Status: Fully Implemented (signing bypassable by env var β fixed this session)
- Production path: Yes
- Reachability: production
- Evidence:
- ML-DSA-65 + Ed25519 hybrid signing:
meow_decoder/manifest_signing.pyβsign_manifest(),verify_manifest_signature() - Encode integration:
meow_decoder/encode.pyL356-401 β signature appended as multi-part QR frames - Decode integration:
meow_decoder/decode_gif.pyL730-757 β unsigned manifests rejected (fail-closed) when signing enabled - PQ ratchet beacon:
meow_decoder/ratchet.pyL470-513 β_fold_pq_into_root()folds ML-KEM-1024 shared secret into root key via HKDF - PQ beacon frame:
meow_decoder/pq_ratchet_beacon.pyβ ML-KEM-1024 encapsulation/decapsulation
- ML-DSA-65 + Ed25519 hybrid signing:
- Remaining weakness:
- Fixed this session (was Critical β now Medium):
MEOW_MANIFEST_SIGNING=offpreviously allowed disabling signing in production mode. Now blocked whenMEOW_PRODUCTION_MODE=1andMEOW_TEST_MODEis not set. Seeencode.pyL346-355,decode_gif.pyL592-598. - Observed (Low): PQ-only fallback (no X25519) mixes PQ shared secret into message key but does NOT perform root rotation (
ratchet.pyL1168-1178). Documented limitation.
- Fixed this session (was Critical β now Medium):
- Status: Fully Implemented
- Production path: Yes β activated via
--password-mode secure-keyboardor--password-mode mouse-gesture - Reachability: production (when CLI flag set)
- Evidence:
- Full tkinter GUI:
meow_decoder/secure_keyboard.pyβ randomized key layout, timing jitter, decoy character injection MouseGesturePassword: same file β BLAKE2b hashing of gesture coordinatesSecureStringclass withbytearrayzeroization on__del__- Imported by both
encode.pyanddecode_gif.py
- Full tkinter GUI:
- Remaining weakness: None significant. CLI fallback via
getpasswithtiming_normalized_input()is acceptable.
- Status: Partially Implemented β code exists but is NOT wired into production paths
- Production path: No
- Reachability: unreachable (from production encode/decode)
- Evidence:
- Module:
meow_decoder/tamper_detection.pyβprotect_functiondecorator,TamperDetector,_self_check() - Observed (High): Neither
encode.pynordecode_gif.pynor__main__.pyimportstamper_detection. Verified via grep β zero references. - Observed (Medium):
_save_state()silently swallowsIOError(tamper_detection.pyL321) β tamper state persistence can be blocked by an attacker. - Observed (Medium):
TamperState.to_bytes()storesstate_keyin plaintext alongside the HMAC value (tamper_detection.pyL207).
- Module:
- Remaining weakness: The entire module is dead code in production. Zero runtime integrity protection.
- Status: Fully Implemented
- Production path: Conditional β activated when steganography is enabled in PARANOID stealth level
- Reachability: production (when stego + PARANOID mode enabled)
- Evidence:
- Integration:
meow_decoder/stego_advanced.pyL525-537 β importsadversarial_carrier.adversarial_embed(), applies noise per-frame with algorithm rotation - Carrier module:
meow_decoder/adversarial_carrier.pyβ sensor, texture, DCT, combined noise generators - Session-unique seeding:
session_seed = secrets.token_bytes(32)+ per-frame SHA-256 derivation
- Integration:
- Remaining weakness:
- Observed (Low):
ImportErroronadversarial_carrieris silently caught (stego_advanced.pyL536-537) β if the module fails to import, paranoid mode silently degrades to standard embedding.
- Observed (Low):
- Status: Fully Implemented
- Production path: Yes β via
--shamir-split THRESHOLD TOTALCLI flag - Reachability: production
- Evidence:
- Module:
meow_decoder/shamir_split.pyβ GF(2^8),split_gif_to_files(),combine_files_to_files(), CLI workflow - Encode integration:
meow_decoder/encode.pyL1891-1902 β post-encode split, original GIF deleted - Duplicate share rejection, checksum verification on combine
- Module:
- Remaining weakness:
- Observed (Low): Windows binary mode set at module import level (
shamir_split.pyL460) β affects any module that imports shamir_split, not just CLI use. - Observed (Low):
set_idis random 4-byte tag (checksum only, not HMAC-bound). Acceptable for intended use.
- Observed (Low): Windows binary mode set at module import level (
- Status: Fully Implemented
- Production path: Conditional
- Reachability: production
- Evidence:
- PyInstaller spec:
meow_decoder.specβ single-binary packaging - Environment safety:
meow_decoder/env_safety.pyβ VM/debugger/container detection, activated byMEOW_STRICT_ISOLATION=1 - Entry point:
meow_decoder/__main__.pyβ callsrequire_safe_environment()when env var set
- PyInstaller spec:
- Remaining weakness:
- Observed (Medium): Every individual env check has
except Exception: pass(env_safety.pyL260). An attacker who triggers exceptions in specific checks bypasses those detections. - Observed (Low): Process detection via
ps axis easily evadable by renaming processes. Defense-in-depth only.
- Observed (Medium): Every individual env check has
- Status: Partially Implemented
- Production path: CI-gated (structural checks only for Verus; real invocation for TLA+, ProVerif, Tamarin, Lean 4)
- Reachability: test-only (formal verification) / docs-only (bounty)
- Evidence:
- Verus proofs:
verus_guarded_buffer.rs,verus_kdf_proofs.rs,aead_wrapper.rs,verus_windows_guard.rsβ structural annotations present, NOT Z3-checked in CI - TLA+/ProVerif/Tamarin/Lean 4: CI-gated with real tool invocation per
docs/SECURITY_INVARIANTS.mdL39-64 - Bounty: SECURITY.md references responsible disclosure β no formal bounty program found in README or SECURITY.md
- Verus proofs:
- Remaining weakness:
- Observed (Medium): Verus proof AEAD-007 domain separation checks byte positions [0..4] but the implementation places the random prefix at bytes [8..12] (
aead_wrapper.rsL185-187 vs L688). Proof specification doesn't match implementation. - Inferred (Low): No formal bug bounty program with monetary rewards found. SECURITY.md has responsible disclosure process only.
- Observed (Medium): Verus proof AEAD-007 domain separation checks byte positions [0..4] but the implementation places the random prefix at bytes [8..12] (
- Status: Sound with defense-in-depth
- Findings:
- Observed (Low):
Nonce::random()inpure_crypto.rsL152-157 generates fully random 12-byte nonces with birthday bound at ~2^48. Production paths use freshsecrets.token_bytes(12)per encryption. - Observed (Information): Python-side nonce reuse guard with 10K LRU cache in
crypto.pyL292-315. Best-effort per-process only. - Observed (Information): Counter-based
NonceManagerinaead_wrapper.rsL120-190 uses 8-byte counter + 4-byte random prefix. Strictly monotonic β cannot collide. - Fixed this session (Medium):
encrypt_raw()/decrypt_raw()inaead_wrapper.rsL375-410 bypassed nonce tracking and werepub. Now#[doc(hidden)]with clear warnings.
- Observed (Low):
- Severity: Low (random nonces used sparingly; counter-based in high-volume paths)
- Reachability: production
- Status: Strong
- Findings:
- Observed (Information): 4 presets in
argon2_presets.py: paranoid (512 MiB/20 iter), balanced (256 MiB/8), activist-fast (194 MiB/4), test (32 MiB/1). Production default is paranoid (8x OWASP). - Observed (Information): Handle-based derivation in
crypto.pyL506-555 β key NEVER enters Python memory. - Observed (Information): Duress key domain-separated via HKDF info
meow_duress_tag_v2incrypto.pyL378-389. - Observed (Low):
pure_crypto.rsargon2_derivehas no minimum password length check (pure_crypto.rsL429). Python layer enforces 8-char NIST minimum upstream.
- Observed (Information): 4 presets in
- Severity: Low
- Reachability: production
- Status: Strong β correctly implemented PQXDH-style integration
- Findings:
- Observed (Information): PQ fold uses HKDF with X25519 root as salt, PQ shared secret as IKM, epoch binding in info β
ratchet.pyL470-513. Both inputs required for prediction. - Observed (Information): 10 unique HKDF domain separation constants. Bounded skip-key cache (MAX_SKIP_KEYS = 2000).
- Observed (Information): Header encryption with XOR-masked frame indices.
- Observed (Low): Key commitment tags (16 bytes) prevent invisible salamanders. Tamarin model:
formal/tamarin/MeowKeyCommitment.spthy.
- Observed (Information): PQ fold uses HKDF with X25519 root as salt, PQ shared secret as IKM, epoch binding in info β
- Severity: Low
- Reachability: production
- Status: Strong with one mitigated bypass
- Findings:
- Observed (Information): 8-field canonical AAD in
crypto.pyL118-153:AAD_VERSION || orig_len || comp_len || salt || sha256 || magic || [mode_byte] || [ephemeral_pk] || [pq_ciphertext] - Observed (Information): HMAC verification is mandatory before any field use. Manifest HMAC covers ALL packed fields including cipher_len, block_size, k_blocks, duress_tag.
- Observed (Information): Decode path rejects unsigned manifests when signing enabled β fail-closed at
decode_gif.pyL749-757. - Fixed this session (was Critical):
MEOW_MANIFEST_SIGNING=offbypass now blocked in production mode atencode.pyL346-355 anddecode_gif.pyL592-598. - Observed (Medium): Rust
aad: Option<&[u8]>inpure_crypto.rsL246-267 allowsNone(empty AAD). Violates stated invariant "aad=None is never allowed". Python layer always provides non-empty AAD.
- Observed (Information): 8-field canonical AAD in
- Severity: Medium (AAD bypass possible at Rust API level, mitigated by Python layer)
- Reachability: production
- Status: Strong (Rust layer); Best-effort (Python layer)
- Findings:
- Observed (Information): All key types use
#[derive(Zeroize, ZeroizeOnDrop)]βSecretKey,AeadKey,X25519KeyPair,MlKemKeyPair,SecurePin. - Observed (Information):
SecureBox<T>: data zeroized β munlock β munmap/VirtualFree. Correct sequence. - Fixed this session (was Medium):
export_key()test-mode escape eliminated βMEOW_TEST_MODE=1can no longer override production guard. Gate now checks_PRODUCTION_MODEalone atcrypto_backend.pyL645. - Observed (Low):
panic = "abort"preventsDrop(and thus zeroization) on panic paths. Combined withunreachable!()macro usage, any unexpected path aborts without zeroization. - Observed (Low): HSM PIN clone (
hsm.rsL345) βpin.pin.clone()forAuthPinmay leave unzeroed copy in memory.
- Observed (Information): All key types use
- Severity: Low
- Reachability: production
- Status: Strong for critical paths
- Findings:
- Observed (Information): AES-GCM tag comparison via
subtle::ConstantTimeEq. HMAC verification viact_eq.secrets.compare_digest()in Python. - Observed (Information):
constant_time_eqinpure_crypto.rsL588-593 leaks length on mismatch β acceptable for fixed-length MAC/tag comparisons. - Observed (Low): CTR counter addition uses variable-time carry propagation (
pure_crypto.rsL345-351). Counter is not secret β low risk.
- Observed (Information): AES-GCM tag comparison via
- Severity: Low
- Reachability: production
- Status: Mitigated with documented limitations
- Findings:
- Inferred (Low):
opt-level = "s"(Cargo.toml) optimizes for size rather than speed, which may affect constant-time properties of some operations.subtlecrate is designed for this. - Observed (Low):
NonceTrackerusesHashSet::contains(nonce.rsL266-283) β not constant-time, but nonce is not secret. - Observed (Information): Python cannot guarantee true constant-time execution due to GIL, GC, and JIT. The
timing_equalizermodule provides best-effort path timing normalization.
- Inferred (Low):
- Severity: Low
- Reachability: production
- Status: Implemented, integrated at PARANOID stealth level
- Findings:
- Observed (Information): Four noise algorithms (sensor, texture, DCT, combined) with per-frame rotation. Session-unique seed via
secrets.token_bytes(32). - Observed (Information): Integration at
stego_advanced.pyL525-537 β appliesadversarial_embed()per frame with algorithm rotation schedule. - Observed (Low):
ImportErroron adversarial_carrier is silently caught β PARANOID mode degrades without notice. - Assumption (Low): No formal steganalysis resistance evaluation against modern ML-based detectors (e.g., SRNet, YeNet). The doc acknowledges this.
- Observed (Information): Four noise algorithms (sensor, texture, DCT, combined) with per-frame rotation. Session-unique seed via
- Severity: Low
- Reachability: production (PARANOID mode only)
- Status: Partially addressed
- Findings:
- Observed (Medium): Schrodinger mode uses
QuantumNoise = XOR(Hash(Pass_A), Hash(Pass_B))with entropy tests for indistinguishability. However, inter-file correlation (e.g., multiple GIFs from same session) is not addressed. - Observed (Information):
chi_square_test()andpairs_test()in adversarial_carrier verify noise distribution properties. - Inferred (Medium): Cat steganography uses APNG (not GIF, which destroys LSB) β correct engineering choice. But APNG format itself is an unusual file format that could be a steganographic indicator.
- Observed (Medium): Schrodinger mode uses
- Severity: Medium
- Reachability: production (stego modes)
- Status: Implemented
- Findings:
- Observed (Information): Length padding via
metadata_obfuscation.add_length_padding()incrypto.pyL595-601. - Observed (Information): QR codes at fixed 600x600 pixels, fixed 10 FPS default.
- Observed (Information):
decorrelation.pymodule exists for traffic analysis resistance.
- Observed (Information): Length padding via
- Severity: Low
- Reachability: production
- Status: Best-effort, not formally evaluated
- Findings:
- Assumption (Medium): No empirical steganalysis benchmarks against standard tools (StegExpose, SRNet, etc.) were found in the codebase. The adversarial carrier generation is theoretically sound but untested against production steganalysis.
- Observed (Low): Floyd-Steinberg dithering and PSNR quality estimation provide defense-in-depth.
- Severity: Medium (absence of formal evaluation)
- Reachability: production (stego modes)
- Finding: Inner
except Exceptioncaught all dead-man switch errors and continued normal decoding, allowing an attacker who corrupts the deadman.json state file to bypass the decoy mechanism. - Severity: High (was) β Fixed this session
- Evidence:
decode_gif.pyL147-159 β now raisesRuntimeErrorwith fail-closed message. - Reachability: production
- Finding:
hmac_sha256()in Rust returned[0u8; HMAC_SIZE]on the theoretically-unreachable error path. An attacker with a zero-MAC oracle could forge authentication tags. - Severity: Medium (was) β Fixed this session
- Evidence:
pure_crypto.rsL552-555 β now usesunreachable!()which aborts rather than returning a potentially forgeable value. - Reachability: production (unreachable in practice, but defense-in-depth)
- Finding:
sysconf(_SC_PAGESIZE)returning -1 was cast tousize::MAX, causing allocation arithmetic to wrap. - Severity: Low (was) β Fixed this session
- Evidence:
secure_alloc.rsL116-121 β now checks<= 0and returnsMmapFailed(0). - Reachability: production (extremely unlikely but correct defense)
- Finding:
MEOW_TEST_MODE=1could overrideMEOW_PRODUCTION_MODE=1to extract raw key bytes from handles. - Severity: High (was) β Fixed this session
- Evidence:
crypto_backend.pyL638-659 β now gates on_PRODUCTION_MODEalone. - Reachability: production
- Finding:
tamper_detection.pyis complete (528 lines,protect_functiondecorator,TamperDetectorclass) but never imported by production encode/decode paths. - Severity: High (module provides zero runtime protection)
- Evidence:
grep -rn 'import.*tamper_detection' meow_decoder/encode.py meow_decoder/decode_gif.py meow_decoder/__main__.pyβ zero results. - Reachability: unreachable
- Finding:
memory_guard.pyprovidesactivate_memory_guard()andrequire_memory_guard()but neither is called in production pipeline. - Severity: Medium β High (memory locking, core dump prevention, ptrace blocking are all opt-in only)
- Evidence:
grep -rn 'import.*memory_guard' meow_decoder/encode.py meow_decoder/decode_gif.py meow_decoder/__main__.pyβ zero results. - Reachability: unreachable (from default production paths)
- Finding:
forensic_cleanup.pyprovides shell history scrubbing, clipboard clearing, temp file cleanup but is never imported by production paths. - Severity: Low (utility module, not a security control)
- Evidence: Not imported by encode.py, decode_gif.py, or main.py.
- Reachability: unreachable (from production paths; available as standalone tool)
- Finding: When hardware derivation (HSM/TPM) fails, code falls back to software mode unless
--no-hardware-fallbackis set. - Severity: Medium (default behavior is fail-open for hardware security)
- Evidence:
decode_gif.pyL366-370 βexcept Exceptionfalls through to software derivation. - Reachability: production
- Finding:
ml-kem = "0.3.0-pre"andml-dsa = ">=0.1.0-rc.5, <0.2"are pre-release crates. The>=range for ml-dsa could pull untested RCs. - Severity: Low (PQ features are experimental/optional)
- Evidence:
crypto_core/Cargo.toml - Reachability: production (when PQ features enabled)
- Finding: With
panic = "abort", passing > 16KB AAD toFrom<&[u8]>aborts the process with no Drop cleanup. - Severity: Low (caller-controlled input; defensive types.rs has
TryFromrecommendation in comment) - Evidence:
crypto_core/src/types.rsL143-148 - Reachability: production
- Assessment: Exemplary transparency. The Verus/formal verification table correctly distinguishes "STRUCTURAL" (CI grep-based) from real CI-gated invocation. The note at the bottom accurately explains CI limitations.
- Evidence:
docs/SECURITY_INVARIANTS.mdL39-79 - Finding: VERIFIED β no overclaims.
- Assessment: Body text is thorough and honest. Scorecard at the end contradicts the nuanced body.
- Findings:
- Observed (Medium): Memory Forensics marked "STRONG" in scorecard but body text correctly calls it "Platform-dependent" and "best-effort" (
THREAT_MODEL.mdL905 vs L559-575). Should be "MITIGATED". - Observed (Medium): Timing Attacks marked "STRONG" but body says Python "cannot guarantee true constant-time execution" (
THREAT_MODEL.mdL906 vs L577-597). Should be "MITIGATED". - Observed (Medium): OS Artifact Leakage marked "STRONG" but
forensic_cleanup.pyis "best-effort and OS-dependent" and never called in production paths (THREAT_MODEL.mdL907).
- Observed (Medium): Memory Forensics marked "STRONG" in scorecard but body text correctly calls it "Platform-dependent" and "best-effort" (
- Findings:
- Observed (Medium): Verus properties table at
SECURITY.mdL404-408 says "Verus-proven" for nonce uniqueness, auth-gated plaintext, key zeroization, no-bypass. CI does not run Z3. Should say "Verus-specified (structural CI, local Z3)". - Observed (Low): Test count discrepancies β documentation says one count,
cargo testproduces a different count. Should be consistent with one methodology.
- Observed (Medium): Verus properties table at
- Assessment: VERIFIED β no significant issues. Protocol description matches implementation.
- Evidence:
docs/PROTOCOL.md
- Findings:
- Observed (Low): "Military-grade authenticated encryption" at
README.mdL195 β marketing overclaim. AES-256-GCM is solid but "military-grade" is meaningless without formal audit/FIPS certification. - Observed (Low): Mobile app claims "267 passing, >=95% coverage" β cannot be verified in the devcontainer.
- Observed (Low): "Military-grade authenticated encryption" at
This is a conservative score reflecting the gap between the strong crypto foundations and the incomplete production integration.
- Crypto foundations are excellent: Handle-based Rust backend keeps secrets out of Python, AES-256-GCM with 8-field AAD binding, Argon2id at 8x OWASP, HMAC-before-use, fail-closed on auth failures.
- Ratchet is well-engineered: PQXDH-style PQ fold, 10 domain-separation constants, bounded skip-key cache, header encryption, key commitment tags.
- Formal verification coverage is genuine and honestly disclosed: TLA+, ProVerif, Tamarin, Lean 4 β all CI-gated with real tool invocation. Verus limitations correctly labeled "STRUCTURAL".
- Multiple defense layers: Fountain codes for loss tolerance, Schrodinger mode for deniability, Shamir splitting for physical redundancy.
- Tamper detection is dead code: An entire security module providing runtime integrity protection is never imported in production. This is not a partial implementation β it's zero implementation in the paths that matter.
- Memory guard is opt-in only: Guard pages, mlock, core dump prevention, ptrace blocking are all available but never activated by default in encode/decode paths.
- 3 documentation overclaims: THREAT_MODEL.md scorecard inflates ratings vs. its own body text. SECURITY.md overstates Verus as "proven" when CI only does structural checks.
No. The cryptographic primitives and protocol design are sound, but three remaining High-severity items must be addressed:
- Tamper detection must be wired into production paths β or removed to eliminate false confidence
- Memory guard should be activated by default in encode/decode (at least best-effort
activate_memory_guard()) - Hardware key derivation fallback should be fail-closed by default β software fallback should require explicit
--allow-software-fallbackflag
| ID | Severity | Finding |
|---|---|---|
| 4.5 | High | Tamper detection is dead code β zero runtime protection |
| 4.6 | MediumβHigh | Memory guard never called in production paths |
| 4.8 | Medium | Hardware derivation fails open to software |
| 1.8 | Medium | Verus AEAD-007 proof checks wrong byte positions |
Wire tamper detection and memory guards into the default production pipeline, make hardware-derivation fallback opt-in rather than default, and downgrade the THREAT_MODEL.md scorecard entries to match the honest body text.
Bugs fixed during this audit session:
- Dead-man switch fail-open β fail-closed (
decode_gif.pyL147-159) MEOW_MANIFEST_SIGNING=offproduction bypass β blocked (encode.pyL346-355,decode_gif.pyL592-598)export_key()test-mode escape β production gate hardened (crypto_backend.pyL638-659)- HMAC zero-MAC on unreachable path β
unreachable!()abort (pure_crypto.rsL552-555) sysconfunchecked cast β fail-closed check (secure_alloc.rsL116-121)