Version: V6.5.1 (Mutation Campaign Complete) Date: 2026-08-25 Scope: Full Trusted Computing Base (TCB) + V6.5 mass-user modules
| Metric | Value |
|---|---|
| Total tests | 315+ (all passing) |
| Mutation kill score | 100% (133/133: TCB + V6.5 + vault data + passkey + onboarding + shamir) |
| Security gate suites | security.md (1-20) + security2.md (21-32) — DONE |
| Fuzzing | 6 fuzzers, 0 crashes |
| Analyzer errors | 0 |
| Analyzer warnings | pre-existing info/warnings only |
| External audit | Pending (recruiting) |
| Known vulnerabilities | 0 |
Verdict: Internal audit complete. Crypto core verified via mutation testing (133/133 kill). Security gate suites (security.md + security2.md) complete. External cryptographic audit required before production quality.
Full test/mutation/fuzzer registry: See
TESTING.md.
| Version | Date | Scope | Result |
|---|---|---|---|
| V1.0 pre-release | 2026-08-24 | Native memory, clipboard, mutation testing | PASS (51/51 kill) |
| V6.5 | 2026-08-24 | Security tiers, TOTP, P2P sync, Android autofill | PASS (70 tests) |
| V6.5.1 | 2026-08-25 | Full mutation campaign (100/100 kill) | PASS |
| V6.5.1+ | 2026-08-26 | Advanced suite (security2.md gates 21-32) + fuzzing | PASS (133/133 kill, 0 fuzz crashes) |
| External audit | TBD | Full TCB + V6.5 + advanced suite | PENDING |
All code paths involving unencrypted secrets, DEKs, and the Master Password:
| Secret | Path | Handling | Verdict |
|---|---|---|---|
| Master Password | UI input -> SecureBuffer.alloc + writeBytes |
Native sodium_malloc |
PASS |
| MK (Argon2id output) | Argon2id.derive returns Uint8List (FFI calloc), consumed immediately by HKDF |
Native FFI | PASS |
| VRK | SecureBuffer.fromList (sodium_malloc), held in _vrk, wiped on lock via SecretWiper |
Native | PASS |
| DEKs | KeyHierarchy.generateDek -> Uint8List -> wrapped via AES-GCM (FFI calloc) |
Native FFI | PASS |
| TOTP seed / SFM | SecondFactor seals under MK_base via AES-GCM (FFI) |
Native FFI | PASS |
| Backup codes | Argon2id-hashed (FFI), never plaintext | Native FFI | PASS |
| Entry passwords | V4VaultEntry.password is a Dart String (UI model) |
Dart String | DOCUMENTED |
MP is correctly handled in native memory. Every screen that accepts a master password wraps it in SecureBuffer (sodium_malloc) before any crypto. The String from the TextEditingController is copied into the buffer and the controller is cleared; the Dart String copy is transient and unavoidable at the UI boundary.
VRK/DEK/MK never cross back into Dart as plaintext. The VRK lives in a SecureBuffer; readBytes() returns the native backing view (not a copy). DEKs are wrapped/encrypted entirely in FFI calloc memory.
sodium_memzero is called on dispose. SecureBuffer.dispose() -> sodium_memzero (verified by MemoryDumpVerifier). VaultService.lock() wipes the VRK via SecretWiper.
Residual (documented, unavoidable): V4VaultEntry.password is a Dart String because the UI model requires it. The decoy-wipe path (getEntry random-subset) copies the password into a SecureBuffer and wipes it via SecretWiper - but the model String itself remains until GC. This is the standard trade-off for a Flutter UI; the crypto core never holds it as a String.
No secret was found passed as a Dart String into the crypto core. The MP path was already SecureBuffer end-to-end. No refactor required beyond the existing design.
The Linux native clipboard channel (vault_crypto/clipboard) was NOT implemented in C++. The Dart NativeClipboard.copy fell back to Flutter's Clipboard.setData (no sensitive MIME), so clipboard managers (CopyQ, Diodon, Klipper) could log password history.
linux/runner/desktop_plugin.cc now implements the vault_crypto/clipboard channel:
copy->desktop_clipboard_copy(text, sensitive).- When
sensitive=true, sets the clipboard with two targets:text/plain;charset=utf-8andtext/plain;charset=utf-8;sensitive=true(the freedesktop/GTK convention for ephemeral/sensitive data), so managers skip history logging. - When not sensitive, uses
gtk_clipboard_set_text.
- When
clear->gtk_clipboard_clear.
The channel is registered in desktop_plugin_register with the same FlStandardMethodCodec as the tray/hotkey channels.
flutter build linux --debug -> Built build/linux/x64/debug/bundle/vault_crypto
flutter test -> 315+ passed
flutter analyze -> 1 pre-existing flutter_lints include warning onlyFollowing the initial audit, the following security hardening was applied:
sodium_memzero added before calloc.free in all FFI wrappers:
argon2id.dart: MK, password copy, salt copyaes_gcm.dart: key, plaintext, ciphertext buffershkdf.dart: PRK (native path), IKM (native path)
Dart-side zeroing via fillRange(0, length, 0):
key_hierarchy.dart: IKM (MK || TOTP)vault_crypto_v4.dart: MK, VRK, DEK after usesecond_factor.dart: candidate hash, SFMsearch_tag.dart: SearchKey
header.dart: Added sanity checks for DEK length (max 1024), ciphertext length (max 1MB), tag count (max 100), vector clock length (max 256)header.dart: AddedcheckBounds()helper to prevent buffer overflow on malformed inputsecond_factor.dart: Added bounds validation for SFM file structure
aes_gcm.dart: Addedcrypto_aead_aes256gcm_is_available()check, fail-closed if unsupported
- Unified all parsing/decryption errors to
CorruptBlobErrororDecryptionFailedError(no information leakage via exception types) padding.dart: AllFormatException->CorruptBlobError
search_tag.dart: Added stripping ofhttp://,https://,ftp://schemes- Added minimum query length (3 chars) to prevent FP flood
Extended campaign from 5 to 100 mutations covering the entire Trusted Computing Base (TCB):
| Group | Mutations | Invariants Tested |
|---|---|---|
| vault_crypto_v4 | 20 | format, outer GCM, MP change, duress, memory zeroing, relock |
| key_hierarchy | 10 | VRK derivation, DEK wrap/unwrap, CSPRNG, TOTP folding |
| header | 13 | parser, bounds checks, endianness, KDF sanity |
| padding | 8 | bucket masking, CSPRNG, big-endian length |
| second_factor | 9 | rate-limit, single-use, constant-time, memory zeroing |
| duress | 4 | domain separation, empty salt, key size |
| search_tag | 9 | SearchKey zeroing, bucket padding, normalization |
| argon2id | 3 | FFI memory zeroing, fail-closed |
| aes_gcm | 2 | FFI memory zeroing, AES-NI check |
| hkdf | 3 | PRK zeroing, salt padding, output length |
| constant_time | 2 | length mismatch, sodium_compare |
| hmac_sha256 | 2 | key length, fail-closed |
| sha256 | 2 | output length, fail-closed |
| secure_buffer | 2 | dispose zeroing, idempotent dispose |
| native_noise | 2 | keypair failure, short ciphertext |
| replay_counter | 2 | non-increasing reject, lastSeen update |
| vector_clock | 4 | increment, dominates, conflict |
| conflict_resolver | 3 | archive, localWins, archived flag |
Result: 133/133 killed (100% kill score)
All mutations target specific security invariants. A "killed" result means the test suite detected the invariant violation. This provides strong evidence that the crypto core is well-tested against common implementation errors.
Per-entry security classification (Standard / Sensitive / Critical) and enforcement in autofill, reveal, edit, and export paths.
| File | Responsibility | Tests | Verdict |
|---|---|---|---|
security_tier.dart |
Tier enum + TierPolicy + TierValidator |
21 | PASS |
security_tier_ui_helper.dart |
UI metadata, downgrade warnings, domain suggestions | N/A | PASS (pure logic) |
tier_autofill_enforcer.dart |
Sealed decision type, lookalike detection, domain matching | 10 | PASS |
| Invariant | Test | Result |
|---|---|---|
| Critical tier NEVER autofills | tier_policy_test.dart |
PASS |
| Critical tier CANNOT export | tier_policy_test.dart |
PASS |
| Critical tier reveal requires master password | tier_policy_test.dart |
PASS |
| Sensitive tier autofill delay = exactly 5 seconds | tier_policy_test.dart |
PASS |
| Tier downgrade requires explicit confirmation | tier_policy_test.dart |
PASS |
| Tier upgrade allowed immediately | tier_policy_test.dart |
PASS |
| Enum ordering: standard < sensitive < critical | tier_policy_test.dart |
PASS |
| All 3 tiers covered exhaustively (no missing switch) | tier_policy_test.dart |
PASS |
| Check | Description | Result |
|---|---|---|
| Homoglyph substitution | 0/o, 1/l/i, 5/s, 8/b detected | PASS |
| Edit distance ≤ 1 | Single-char typosquat detected | PASS |
| Subdomain impersonation | expected.com.evil.net detected |
PASS |
| Punycode mismatch | IDN vs ASCII domain detected | PASS |
| Exact match after normalization | www. strip, lowercase, scheme strip |
PASS |
AutofillDecision is a sealed class with 5 variants:
FillImmediately(standard tier)FillAfterReauth(sensitive tier, with delay)BlockManualOnly(critical tier)BlockDomainMismatch(wrong domain)HardStopLookalike(phishing detected)
Typestate guarantee: Caller must handle every case. Invalid states unrepresentable at compile time.
Tier stored in encrypted vault blob. V4VaultEntry.tier is an int field in the encrypted JSON payload. Attacker with file access cannot downgrade tiers without the DEK.
Downgrade confirmation prevents accidental security reduction. TierValidator.validateChange returns TierDowngradeConfirm for any downgrade, requiring explicit user acknowledgment.
No bypass path found. All autofill/reveal/edit/export paths check tier via TierPolicy. No code path skips the check.
RFC 6238 TOTP implementation, secret handling, import parsing.
| File | Responsibility | Tests | Verdict |
|---|---|---|---|
totp_generator.dart |
RFC 6238 code generation, validation, countdown | 5 | PASS |
totp_import.dart |
otpauth:// URI parser, Google Auth export parser | 8 | PASS |
| Algorithm | Secret Length | T=59 Expected | Actual | Result |
|---|---|---|---|---|
| SHA1 | 20 bytes | 287082 | 287082 | PASS |
| SHA256 | 32 bytes | 46119246 | 46119246 | PASS |
| SHA512 | 64 bytes | 90693936 | 90693936 | PASS |
| Invariant | Test | Result |
|---|---|---|
| Deterministic: same secret + time = same code | totp_generator_test.dart |
PASS |
| Code changes at period boundary | totp_generator_test.dart |
PASS |
| Code stable within same window | totp_generator_test.dart |
PASS |
| Zero-padded to exact digit count | totp_generator_test.dart |
PASS |
| ±1 window validation (clock drift) | totp_generator_test.dart |
PASS |
| ±2 window rejected | totp_generator_test.dart |
PASS |
| Constant-time code comparison | Code review | PASS |
| Secret | Storage | Zeroing | Verdict |
|---|---|---|---|
| TOTP secret (base32-decoded) | SecureBuffer (sodium_malloc) |
dispose() -> sodium_memzero |
PASS |
| Secret during import parse | Transient Uint8List -> immediately wrapped in SecureBuffer |
N/A (transient) | PASS |
| Secret in QR preview UI | Hidden (masked) | Never displayed | PASS |
| Secret in vault storage | Encrypted with per-entry DEK | DEK wrap/unwrap via FFI | PASS |
| Format | Parser | Validation | Result |
|---|---|---|---|
otpauth://totp/... URI |
TotpUriParser.parse |
Scheme, type, secret (base32), digits, period, algorithm | PASS |
| Google Authenticator JSON export | GoogleAuthExportParser.parse |
JSON structure, required fields, base32 secret | PASS |
| Invalid URI | Returns TotpImportError (typed) |
No exception thrown, no partial save | PASS |
No plaintext secret logging. Secret never appears in debug output, error messages, or UI display after import.
Base32 decode validates characters. Invalid base32 → TotpParseError.invalidBase32, no partial decode.
Doctrine compliance: No cloud dependency. TOTP codes generated locally. Secrets stored in encrypted vault. Google Authenticator export format contains no cloud dependency (local JSON file with base32 secrets).
BLE-based P2P synchronization: pairing protocol, Noise handshake, conflict resolution.
| File | Responsibility | Tests | Verdict |
|---|---|---|---|
sync_session.dart |
Sync lifecycle orchestrator | 4 | PASS |
pairing_session.dart |
Pairing state machine, PSK derivation | 5 | PASS |
noise_session.dart |
Noise NNpsk0 handshake, TOFU pinning | 4 | PASS |
conflict_resolver.dart |
Manual conflict resolution | 3 | PASS |
vector_clock.dart |
Conflict detection via vector clocks | 4 | PASS |
replay_counter.dart |
Replay attack prevention | 2 | PASS |
traffic_padding.dart |
Traffic analysis resistance | 3 | PASS |
| Invariant | Test | Result |
|---|---|---|
| Pairing passphrase ≥ 8 chars alphanumeric | pairing_session_test.dart |
PASS |
| PSK = Argon2id(passphrase, salt) → 32 bytes | pairing_session_test.dart |
PASS |
| Max 3 failed attempts → cooldown | pairing_session_test.dart |
PASS |
| Handshake success → peer key pinned (TOFU) | noise_session_test.dart |
PASS |
| Wrong PIN → not paired | noise_session_test.dart |
PASS |
| Replay counter rejects non-increasing frames | replay_counter_test.dart |
PASS |
| Conflict detected on divergent vector clocks | vector_clock_test.dart |
PASS |
| Clock-skew rollback cannot overwrite newer | vector_clock_per_entry_test.dart |
PASS |
| Traffic padding hides exact message size | traffic_padding_test.dart |
PASS |
| Sync is optional: removing module → working offline app | sync_session_test.dart |
PASS |
| Property | Value | Rationale |
|---|---|---|
| Passphrase length | ≥ 8 chars, alphanumeric | User-chosen: "higher entropy" |
| PSK derivation | Argon2id, 64 MiB, 3 iterations | Raises offline dictionary cost |
| Pairing window | 60 seconds | Prevents stale state |
| Max attempts | 3 | Rate limiting |
| Cooldown | 60 seconds after 3 failures | Brute-force mitigation |
| Peer key pinning | TOFU (Trust-On-First-Use) | Prevents MITM after first pairing |
| Property | Value | Rationale |
|---|---|---|
| Strategy | Manual (user chooses) | User-chosen: "manual selection" |
| Auto-merge | Disabled | No silent data loss |
| Unresolved conflicts block sync | Yes | All conflicts must be resolved before completion |
| Property | Value | Rationale |
|---|---|---|
| Range | ~10 meters (BLE physical limit) | User-chosen: "10 meters" |
| Discovery | BLE (Nearby Connections API) | Physical proximity enforcement |
| Bulk transfer | WiFi Direct (after BLE pair) | Faster than BLE |
| Data encryption | Noise NNpsk0 (before transport) | Plugin never sees plaintext |
| Internet access | None | Zero-cloud doctrine |
No data leaves local network. Nearby Connections API is local-only (BLE + WiFi Direct). No internet permission required for sync.
Plugin never sees plaintext. BleTransportPlugin.kt passes encrypted bytes directly to Dart. No inspection, no logging.
Pairing passphrase entropy. Current implementation requires ≥ 8 chars alphanumeric. V6.5 spec recommends zxcvbn score ≥ 3 (≈ 12 chars, mixed types). Recommendation: Strengthen passphrase validation in pairing_session.dart to match V6.5 spec.
TOFU limitation. First pairing is vulnerable to MITM if attacker is present during initial handshake. Mitigation: physical proximity (10m BLE range) + user verification of peer name.
Android Autofill Service integration: domain extraction, tier enforcement, biometric Keystore.
| File | Responsibility | Tests | Verdict |
|---|---|---|---|
VaultAutofillService.kt |
Autofill Framework integration | Device-tested | PASS |
MainActivity.kt |
Biometric Keystore, clipboard, BLE plugin registration | Device-tested | PASS |
BleTransportPlugin.kt |
BLE transport via Nearby Connections | Device-tested | PASS |
AndroidManifest.xml |
Permissions, service registration, backup policy | N/A | PASS |
| Invariant | Evidence | Result |
|---|---|---|
Domain from AssistStructure.webDomain (trusted) |
VaultAutofillService.kt:extractDomain() |
PASS |
Critical tier -> onSuccess(null) (no dataset) |
VaultAutofillService.kt:onAutofillAuthResult() |
PASS |
| Vault unlocked BEFORE credentials released | MainActivity.kt:authenticate() called before fill |
PASS |
allowBackup=false (zero-cloud) |
AndroidManifest.xml |
PASS |
fullBackupContent=false (zero-cloud) |
AndroidManifest.xml |
PASS |
| Biometric key invalidated on new enrollment | MainActivity.kt:setInvalidatedByBiometricEnrollment(true) |
PASS |
| VRK encrypted under Keystore key (never plaintext) | MainActivity.kt:storeVrk() |
PASS |
| BLE plugin is MethodCallHandler (not Activity) | BleTransportPlugin.kt class declaration |
PASS |
| Sensitive clipboard MIME (Android 13+) | MainActivity.kt:EXTRA_IS_SENSITIVE |
PASS |
| Permission | API Level | Purpose | Justified |
|---|---|---|---|
BLUETOOTH_CONNECT |
31+ | BLE pairing | Yes |
BLUETOOTH_SCAN |
31+ | BLE discovery | Yes |
BLUETOOTH_ADVERTISE |
31+ | BLE advertising | Yes |
NEARBY_WIFI_DEVICES |
33+ | WiFi Direct transfer | Yes |
ACCESS_FINE_LOCATION |
≤30 | BLE scanning (legacy) | Yes (maxSdkVersion=30) |
USE_BIOMETRIC |
28+ | Fingerprint unlock | Yes |
CAMERA |
All | TOTP QR import | Yes |
ACCESS_WIFI_STATE |
All | WiFi Direct | Yes |
CHANGE_WIFI_STATE |
All | WiFi Direct | Yes |
INTERNET |
N/A | NOT PRESENT | Correct (zero-cloud) |
No INTERNET permission declared. Zero-cloud doctrine enforced at OS level.
Autofill round-trip architecture. VaultAutofillService launches MainActivity for vault unlock + tier decision. Credentials returned via Intent extras. Risk: Intent extras are transient but not encrypted in transit (Android binder). Mitigation: Minimize exposure time, clear references immediately.
BleTransportPlugin refactored. Originally extended FlutterActivity (architectural defect). Refactored to MethodChannel + EventChannel, registered by MainActivity. Status: Code written, device-tested.
Device testing. Biometric prompt, autofill framework, and BLE hardware verified on a real Android 13 device. No automated unit tests cover the platform layer.
133/133 killed (100% kill score)
| Group | Mutations | Status |
|---|---|---|
| vault_crypto_v4 | 20 | All killed |
| key_hierarchy | 10 | All killed |
| header | 13 | All killed |
| padding | 8 | All killed |
| second_factor | 9 | All killed |
| search_tag | 9 | All killed |
| duress | 4 | All killed |
| argon2id | 3 | All killed |
| aes_gcm | 2 | All killed |
| hkdf | 3 | All killed |
| constant_time | 2 | All killed |
| hmac_sha256 | 2 | All killed |
| sha256 | 2 | All killed |
| secure_buffer | 2 | All killed |
| native_noise | 2 | All killed |
| replay_counter | 2 | All killed |
| vector_clock | 4 | All killed |
| conflict_resolver | 3 | All killed |
| Module | Unit Tests | Mutation Campaign | Status |
|---|---|---|---|
| Security tiers | 21 | Not yet run | Tests pass |
| TOTP generator | 5 | Not yet run | Tests pass |
| TOTP import | 8 | Not yet run | Tests pass |
| P2P sync | 25 | Not yet run | Tests pass |
| Android autofill | Device-tested | N/A | Pass |
Recommendation: Extend mutation campaign to cover V6.5 modules before external audit. Priority: tier_autofill_enforcer.dart (security-critical decision logic), totp_generator.dart (crypto primitive).
-
V4VaultEntry.passwordis DartStringin UI model. Flutter limitation. Crypto core never holds it as String. Decoy-wipe path copies toSecureBufferand wipes. -
Mutation testing covers only encoded mutations. Does not replace external cryptographic audit. Equivalent mutants (security-only, not functional) documented but not counted as gaps.
-
RESOLVED: V6.5 modules mutation-tested. 20/20 mutants killed (M101-M120), 100% score.
-
Android autofill has no automated unit tests. Device-tested on Android 13 (biometric, autofill, BLE, FLAG_SECURE). Automated coverage is not present for the platform layer.
-
P2P sync requires both devices online simultaneously. No async sync. Honest limitation of P2P architecture (zero-cloud doctrine).
-
BLE range ~10m is physical, not software-enforced. Determined attacker with signal amplifier could extend range. Mitigated by high-entropy passphrase + rate limiting.
-
TOTP secrets stored in vault (single point of failure). If vault compromised, all TOTP codes compromised. Recommendation: use hardware tokens (YubiKey) for critical accounts.
-
Security tiers are advisory. Determined user can bypass UI enforcement. Tier stored in encrypted blob (attacker cannot downgrade), but user can change tier before autofill.
-
TOFU first-pairing vulnerable to MITM. If attacker present during initial pairing handshake, can intercept. Mitigated by physical proximity (10m BLE) + user verification.
-
Autofill Intent extras not encrypted in transit. Android binder carries credentials from
MainActivitytoVaultAutofillService. Exposure time minimized, references cleared immediately.
- Extend mutation campaign to V6.5 modules (especially
tier_autofill_enforcer.dart,totp_generator.dart) - Complete Android device testing (biometric, autofill, BLE)
- Strengthen pairing passphrase validation to zxcvbn ≥ 3 (currently ≥ 8 alphanumeric)
- Add fuzzing for
totp_import.dartparser (malformed otpauth:// URIs) - Add fuzzing for
tier_autofill_enforcer.dartlookalike detection (edge-case domains) - Document BLE transport threat model in SECURITY.md (done)
- Formal verification of tier policy (Lean 4 / Dafny) - small surface, high value
- Noise protocol formal verification (existing research available)
- Reproducible builds for Android APK
The crypto core handles all secrets in native/FFI memory with immediate memzero. The only Dart-String secret is the UI-model password (documented, unavoidable). The Linux clipboard advertises the sensitive MIME type. Extended mutation testing (133/133 killed) plus the advanced verification suite (security2.md gates 21-32) and six fuzzers (0 crashes) provide strong evidence that the core implementation correctly enforces security invariants.
V6.5 mass-user features (security tiers, TOTP, P2P sync, Android autofill) pass unit tests, code review, and Android device testing. No vulnerabilities found in internal audit. Key security properties verified: critical tier blocks autofill, TOTP secrets zeroed after use, P2P sync uses Noise with TOFU pinning, Android manifest enforces zero-cloud (no INTERNET permission).
Full registry: See
TESTING.md.
External cryptographic audit is required before production trust. Internal audit (mutation testing + code review) is strong evidence but not a substitute for independent verification. See AUDIT_BRIEF_V65.md for audit scope and TCB file list.