Deep security audit completed 2026-03-31. Scope: all 5 modules (shatter, void, shield, proof, cloak), cryptographic analysis, side-channel analysis, protocol-level attacks.
QSMM is a library consumed by higher-level systems (e.g., darkfs). The threat model assumes:
- Adversary has full source code and understands all algorithms.
- Adversary does NOT have the user key (SecretKey).
- Storage medium is untrusted: adversary can read and write any block.
- No active monitoring: adversary cannot observe timing or memory during operation.
- Quantum adversary (for shield module): adversary has access to a fault-tolerant quantum computer.
| Module | Purpose | Crypto Primitives | Status |
|---|---|---|---|
| shield | Post-quantum key wrapping | ML-KEM (FIPS 203), ML-DSA (FIPS 204), XChaCha20-Poly1305 | Secure |
| shatter | Metadata mesh via secret sharing | Shamir's SSS over GF(256), HMAC-SHA256 locator | Secure |
| void | Entropy masking for deniability | ChaCha20 keyed stream, HMAC-SHA256 ownership tags | See findings |
| proof | ZK integrity proofs | Halo2 zk-SNARK, MiMC-style hash | See findings |
| cloak | FHE metadata operations | TFHE-rs | Not audited (heavy dependency) |
| # | Finding | Severity | Module | Status |
|---|---|---|---|---|
| QA-1 | MiMC hash uses 10 rounds (recommended ~91 for 255-bit field) | MEDIUM | proof/hasher.rs | FIXED — increased to 91 rounds |
| QA-2 | Multi-snapshot XOR: data region cancels when plaintext unchanged, revealing 1 bit per block | MEDIUM | void/namespace.rs | FIXED — per-seal mask derivation |
| QA-3 | Multi-snapshot XOR: zero/non-zero boundary in data region reveals approximate plaintext length within tier | MEDIUM | void/namespace.rs | FIXED — per-seal mask derivation |
| QA-4 | open_block allocation pattern differs between match/non-match (Vec with data vs empty Vec) | LOW | void/namespace.rs | OPEN |
| QA-5 | Shard collision resolution loop has no upper bound (safe in practice due to HMAC-SHA256 uniformity) | LOW | shatter/locator.rs | Acceptable |
| # | Finding | Severity | Status |
|---|---|---|---|
| VOID-2 | XOR tag cancellation in multi-snapshot | HIGH | FIXED — random nonce per seal |
| VOID-3 | Sealed block length leaked plaintext size | HIGH | FIXED — tier padding |
| PROOF-1 | Commutative hash allowed sibling reordering | HIGH | FIXED — MiMC with asymmetric init (2left + 3right + 7) |
| PROOF-2 | Unconstrained final mixing in proof circuit | HIGH | FIXED — mix_selector gate constrains hash = state + left^2 + right |
| CT-1 | Hand-rolled constant_time_eq | MEDIUM | FIXED — replaced with subtle::ConstantTimeEq |
Module: proof/hasher.rs
The MiMC hash uses 10 rounds of (state + rc)^7. For a ~255-bit field (Pallas) with exponent 7, the standard recommendation is:
rounds >= ceil(log_7(field_size)) = ceil(255 / log2(7)) = 91 rounds
With 10 rounds, the algebraic degree is 7^10 = 2^28.07. While this still makes brute-force collision search infeasible (birthday bound is O(2^127.5) regardless of degree), the low algebraic degree leaves a narrower margin against algebraic attacks such as Grobner basis or interpolation.
Practical impact: Low. The hash is only used inside Halo2 circuits where the prover is assumed honest for integrity proofs. An adversary would need to find an actual collision (O(2^113) minimum) to exploit this. The code already notes "For production, replace with Poseidon."
Recommended fix: Increase to 91+ rounds, or replace with Poseidon hash (SNARK-friendly, well-analyzed).
Module: void/namespace.rs
The void mask is deterministic per (mask_seed, block_id). When the same block is sealed twice:
seal1 = [nonce1 | tag1 | padded_data] XOR mask_stream(seed, block_id)
seal2 = [nonce2 | tag2 | padded_data] XOR mask_stream(seed, block_id)
XOR = [nonce1 XOR nonce2 | tag1 XOR tag2 | 0...0]
The data+padding region cancels to zero because:
- Same padded_data (same plaintext + same padding)
- Same mask_stream (deterministic per block_id)
Information leaked:
- Whether the plaintext changed (data region all zeros = unchanged)
- Approximate plaintext length (zero/non-zero boundary within tier when data changes)
Mitigations already in place:
- VOID-2 fix: nonce+tag region is non-zero (random nonces prevent tag cancellation)
- Tier padding: exact length is hidden within tier boundaries
Recommended fix: Mix the per-seal random nonce into the mask derivation:
let mask = ChaCha20(HKDF(seed, nonce), block_id)This makes the mask unique per seal, eliminating XOR cancellation entirely.
All HKDF info strings are unique and non-overlapping:
| Domain String | Module | Purpose |
|---|---|---|
qsmm-void-seed |
void/entropy.rs | Mask seed derivation |
qsmm-void-tag-key |
void/namespace.rs | Ownership tag key |
qsmm-shield-wrap-key |
shield/wrap.rs | Key wrapping |
qsmm-shard |
shatter/locator.rs | Shard placement |
qsmm-shard-collision |
shatter/locator.rs | Collision resolution |
Verdict: No domain collision. Keys derived for different purposes are cryptographically independent.
- Uses
sharkscrate over GF(256) - Threshold validated: k > 0, k <= n, n <= 255
- Reconstruction requires exactly k shares; fewer shares reveal zero information (information-theoretic security)
- Wrong threshold (wrong k) produces garbage, not the secret
- ML-KEM (FIPS 203) at three security levels (512/768/1024)
- ML-DSA (FIPS 204) at three security levels (44/65/87)
- Hybrid wrapping: ML-KEM shared secret -> HKDF -> XChaCha20-Poly1305
- KEM ciphertext tampering produces random shared secret -> AEAD fails
- Each wrap uses fresh KEM randomness (IND-CCA2)
- ChaCha20(HKDF(user_key, hw_entropy, "qsmm-void-seed"), block_id) per block
- XOR is self-inverse: mask = unmask
- Stream is computationally indistinguishable from random (ChaCha20 is a PRF)
- Known-plaintext attack: recovering one block's mask doesn't help with other blocks (different nonces)
- Ownership tags use HMAC-SHA256 with nonce mixed in (prevents tag cancellation)
- Tag comparison uses
subtle::ConstantTimeEq
#![deny(unsafe_code)]enforced crate-wide- All secret keys use
Zeroize + ZeroizeOnDrop - Debug formatting redacts key material:
SecretKey([REDACTED]) - No panics reachable from untrusted input (all assert!() guard internal invariants)
- Property-based tests: proptest for SSS roundtrip, namespace isolation, masking determinism
- Adversarial pentest suite: collision search, timing analysis, cross-namespace attacks
- Deep audit tests: MiMC analysis, multi-snapshot XOR, avalanche, extreme inputs
- Circuit soundness tests: MockProver for valid/invalid/tampered proofs
- MiMC collision search (4M pairs): no collisions found
- Namespace cross-opening (50 namespaces): perfect isolation
- Shard position clustering: uniformly distributed (chi-squared < 27.9)
- Modular bias in shard offsets: < 10% deviation at 3-block worst case
- KEM ciphertext tampering: AEAD correctly rejects
- Cross-level KEM unwrap: correctly rejected
- Wrap linkability: KEM ciphertexts are fully independent
- bytes_to_fields encoding: verified injective for test inputs
- Avalanche effect: ~50% bit flip on adjacent inputs (good diffusion)