# Per-note and secret-folder encryption: design only (agy gap 5, roadmap 34) ## Research (2026-10-21; agy claims checked) [Apple Platform Security](https://support.apple.com/guide/security/secure-features-in-the-notes-app-sec1782bcab1/web) documents secured notes; it does not establish encrypted folders. [Joplin E2EE](https://joplinapp.org/help/apps/sync/e2ee/) describes encrypted synchronization, not a per-note local-disk lock. Drop agy's implied equivalence and “small effort” rating. AES-256-GCM and Argon2id are candidate choices, not existing NotesCraft capability or an approved dependency list. [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106) defines Argon2 and parameter profiles; it does not remove need for platform cost measurement/security review. ## Goal and non-goal Produce reviewable threat model, versioned container proposal, key lifecycle, integration inventory and approval checklist. NO crypto implementation, Cargo edits, lock UI, production format, or claim of secure encryption in this phase. No new crate now. A later approved implementation plan must separately register any new crate in root `Cargo.toml` and `xtask/src/layers.rs` with explicit layer rationale. ## D1 Threat model Protect locked content against offline theft of vault/ciphertext and unauthorized reading by sync storage. Attacker may read/copy/change/replay encrypted files and observe path, size and timestamps. AEAD detects tampering, not rollback/deletion: rollback needs trusted external state and is out of v1. No protection against compromised OS/app, keylogger, hostile same-user process, unlocked app, clipboard/screenshot/print, swap/core dumps or prior plaintext backups. Do not call local lock “zero knowledge” for all app operations. Whole-disk encryption still recommended as complementary scope, not prerequisite. Per-note visible filename leaks title by default. Secret folder proposal uses opaque container name with encrypted manifest hiding child filenames/tree; container size/timing and existence still leak. Plain external wikilinks, backlink snippets, recent lists and previously indexed metadata can leak. Reject automatic encryption if unmanaged attachments or external plaintext copies would make privacy claim false; explicit migration review required. ## D2 Candidate format (not frozen) Per-note `.md.enc`, secret folder `.ncfolder.enc` (single encrypted manifest plus separately encrypted opaque objects to avoid full-folder rewrite). Container envelope: magic `NCENC`, version `1`, bounded header length, suite/KDF IDs, bounded KDF parameters, salt, key-slot metadata, random object ID, generation, nonce, ciphertext length, ciphertext, AEAD tag. Fixed canonical binary encoding and byte order to be specified in design task before approval; no ambiguous JSON canonicalization. Authenticated associated data covers full immutable header, object identity and purpose; key slots separately authenticate wrapped data key. Payload holds original UTF-8 note and metadata/manifest; attachment bytes encrypted under separate object IDs. Path is not AEAD identity so rename need not decrypt/re-encrypt; object substitution rejected by expected ID and manifest binding. Candidate: AES-256-GCM with fresh cryptographic nonce per write and Argon2id passphrase KEK wrapping random data keys. Never derive nonce from time/counter alone, never reuse across rewrites. Owner must approve RNG, AEAD, KDF, secret-memory and keychain dependencies as a reviewed set. Enforce pre-KDF parameter ceilings and payload bounds before allocation; unknown suite/version and authentication failure fail closed. Proposed ceiling 256 MiB ciphertext per object and 16 KiB envelope; KDF accepted range requires measured profile, not arbitrary caller values. No hand-written crypto or fake test encryption. ## D3 Key lifecycle and state States Locked -> Unlocking -> Unlocked -> Locking -> Locked; authentication error returns Locked. Keys only enter memory after explicit local unlock; never env/settings/logs/control-channel/MCP params. Passphrase change rewraps data keys after old authentication; interrupted replacement preserves previous valid key slot. Forgotten passphrase is unrecoverable unless owner chooses recovery-key design; never password-reset promise. Session-only unlock default; auto-lock on vault switch, screen lock/suspend and configurable idle timeout. Dirty buffers must atomically save encrypted content or visibly refuse lock; failed save cannot discard work or falsely report locked. Clear previews, history, search postings, undo/redo, extracted text, clipboard references, AI context and jobs before locking. Memory zeroization best effort, cannot promise erasure of framework/OS copies. Keychain/biometric persistence is opt-in separate approval; no unlock key synced beside ciphertext. ## D4 Integration and migration boundaries Audit actual `crates/engine/src/vault/session.rs`, `crates/engine/src/vault/buffers.rs`, `crates/engine/src/vault/attachments.rs`, `crates/engine/src/vault/find/content_search.rs`, `crates/engine/src/ai/assistant_gate.rs`, `crates/engine/src/sync/worker.rs`, `crates/vault/src/lib.rs` and UI previews. Capture, OCR, PDF crops, publish/share/export/recovery/trash must never emit plaintext while locked. All programmatic reads denied while locked even if path known; unlocking UI-only, no passphrase command. Unlocked disclosure to AI/plugins/export requires explicit action and documented policy. Secret folder is security domain, not filesystem hidden flag; inherited encryption enforced on move/create, shared attachments require copy into encrypted domain or refusal. Existing plaintext index/cache/recovery/sync versions need inventoried purge/migration, with warning that SSD snapshots/remote history cannot be securely erased by app. Migration journal uses craft-atomic through vault boundary, writes/authenticates ciphertext before removing plaintext; removal is explicit destructive action requiring owner authorization. Crash matrix covers every stage, mixed encrypted/plain states and rollback; never plaintext temp files. Design must decide conflict handling and multi-device key rotation before sync is enabled for encrypted domains. No existing engine command bypass permitted. ## Acceptance criteria (documents only) 1. Threat/asset/adversary matrix distinguishes local at-rest protection, sync E2EE, metadata leakage and excluded attacks. 2. Envelope byte layout, bounds, authenticated fields, nonce uniqueness strategy, key slots, version rejection and test-vector source documented; no implementation or unapproved dependency. 3. State machine covers wrong password, cancellation, dirty save failure, auto-lock, crash and lost password; memory limitations stated honestly. 4. Integration matrix maps every plaintext producer/consumer to existing file, required gate, test and owner; unresolved gaps block implementation. 5. Migration/recovery and hostile-header test plans cover interrupted writes, nonce reuse prevention, tamper, replay limitation and backups; owner decisions recorded explicitly. ## Open owner questions - Q1: Approve exact threat scope, visible per-note names and opaque secret-folder domain proposal? Is hidden tree a requirement? - Q2: Which audited Rust AEAD/KDF/RNG/zeroization/keychain dependencies and security reviewer may be approved? AES-GCM/Argon2id are proposals only; no dependency before response. - Q3: Session-only unlock default, idle policy, OS keychain/biometric opt-in, recovery key or deliberately unrecoverable passphrase? - Q4: Encrypted attachments, multi-device sync, share/publish restrictions and handling historical plaintext copies: required launch scope or explicitly excluded? No implementation until reviewed. ## Feature ideas (researched, ranked) 1. Optional local biometric/keychain unlock, informed by [Apple secured notes](https://support.apple.com/guide/security/secure-features-in-the-notes-app-sec1782bcab1/web), requiring separate safe-platform design. 2. Multi-device wrapped-key onboarding, informed by [Joplin E2EE setup](https://joplinapp.org/help/apps/sync/e2ee/), not a copy of its local-storage guarantees. 3. Measured KDF profiles and migration policy anchored in [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106).