Vibecode Enpass
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Enpass, store an encrypted vault locally and optionally sync it through the user's own cloud folder. The hard boundary is mature apps, browser extensions, platform biometrics, and ongoing security maintenance, plus security assurance, infrastructure, and trust.
You are building a lean indie version of Enpass. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Enpass · indie build A local encrypted vault whose two hard problems are handling keys correctly using primitives you did not invent, and never losing data across writes, sync conflicts and format changes over years of use. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Cryptography | A maintained library with published parameters, no custom constructions | designing this yourself is where password managers fail | | Key derivation | Argon2id at the current published parameters, recorded per vault | parameters change and an old vault must still open | | Storage | A versioned file format written atomically | a half-written vault is a lost vault | | Sync | Through storage the user controls, with conflicts kept rather than resolved | a merge that picks a winner loses a password silently | | Runtime | A local application; the vault never leaves the machine unencrypted | the whole premise is that nobody else can read it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A maintained cryptography library with Argon2id** · free - Why: Every primitive comes from here, and choosing an unmaintained or hand-rolled implementation is the single worst decision available in this project. - Get it: A well-reviewed library for your runtime providing Argon2id and an authenticated encryption mode. - Verify: It produces the published test vectors for both - [ ] **The current password storage guidance** · free - Why: The derivation parameters are copied from it rather than chosen, and they change, so the version you followed needs recording. - Get it: Read the current guidance, note the recommended parameters and the date, and store both in your documentation. - Verify: A written note records the parameters and the guidance version followed - [ ] **Storage you control for sync** (optional) · pennies per month, or free with existing sync - Why: The whole premise is that the vault syncs through the user's own storage rather than a service you operate, which changes the trust model entirely. - Get it: Any file sync you already use, or object storage with a key you hold. - Verify: A file written on one device appears on another - [ ] **Two machines you can take offline** · free - Why: Conflict handling is the part most likely to lose data and it is only testable with two devices editing offline simultaneously. - Get it: A second machine or a virtual machine with its own device identifier. - Verify: Both can edit offline and reconcile against the same remote ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. - mature apps, browser extensions, platform biometrics, and ongoing security maintenance - independent security audits - global relay infrastructure - breach monitoring data - account recovery and support If one of those is essential to you, that is the reason to keep paying for Enpass, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Enpass The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a password vault encrypted on your machine and synced through storage you own to replace Enpass. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and neither is negotiable. Do not design any cryptography. Use vetted primitives through a maintained library, in the modes their documentation specifies. For deriving the vault key from a master password that means a memory-hard function with published parameters: the current guidance is Argon2id at around forty six mebibytes with one iteration, or nineteen mebibytes with two, and if that is unavailable, scrypt or a very high iteration count with a suitable hash. Copy the parameters from the guidance rather than choosing your own, and record which version you followed. Second, a password manager that loses a vault is worse than not having one, because people put everything in it and stop remembering any of it. So the format is versioned from the first release, every write is atomic, backups are automatic and a restore is tested in the suite rather than assumed. More users lose a vault to a corrupted write or a failed sync than to an attacker. The corollary of both: if you cannot decrypt an old vault after a format change, or a half-written file leaves the vault unreadable, the cryptography being correct did not help. ### Stack (fixed, do not substitute) - Cryptography: A maintained library with published parameters, no custom constructions · designing this yourself is where password managers fail - Key derivation: Argon2id at the current published parameters, recorded per vault · parameters change and an old vault must still open - Storage: A versioned file format written atomically · a half-written vault is a lost vault - Sync: Through storage the user controls, with conflicts kept rather than resolved · a merge that picks a winner loses a password silently - Runtime: A local application; the vault never leaves the machine unencrypted · the whole premise is that nobody else can read it ### Data model (create this before Phase 1) - vault_file(format_version, kdf_name, kdf_params_json, salt, nonce, ciphertext, mac) · everything needed to open it later - entries(id, kind, title, fields_json, tags_json, created_at, updated_at, deleted_at) · inside the encrypted payload - history(entry_id, ordinal, fields_json, at) · previous values, because a changed password sometimes needs the old one - attachments(entry_id, id, filename, bytes, blob_ref) · encrypted with the vault, size bounded - sync_state(remote_ref, last_seen_version, last_pushed_version, device_id) - conflicts(id, remote_version, local_version, detected_at, resolved_at, resolution) · both sides retained - The key derivation parameters are stored in the file, so a vault written under old parameters still opens after the recommendations change. ### Phase 1 · A vault you can still open next year Build: Derive the vault key from the master password using a memory-hard function at the currently published parameters. Encrypt with an authenticated mode. Store the algorithm, parameters, salt and format version in the file itself. Trap: Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. Done when: The vault file records its format version, algorithm and derivation parameters, and a vault written under old parameters opens after the defaults change, tested directly. Encryption is authenticated so tampering is detected rather than producing garbage. No custom cryptographic construction exists anywhere, verified by review. The master password is never written to disk or logged. Do not build yet: No entries, sync, autofill or backup. ### Phase 2 · Writes that cannot half happen Build: Write to a temporary file, flush, then replace atomically. Keep the previous version until the new one is verified readable. Take automatic backups and test a restore in the suite. Trap: Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. Done when: A crash injected mid-write leaves the previous vault intact and openable, tested by killing the process during a save. The new file is verified readable before replacing the old one. Automatic backups are kept with a stated retention. A restore from backup is exercised in the test suite rather than documented. Do not build yet: No sync, autofill or sharing yet. ### Phase 3 · Entries, history and the awkward fields Build: Entries with typed fields, tags and search over the decrypted payload in memory. Keep previous field values as history. Support notes, attachments and one-time code secrets with a bounded attachment size. Trap: Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. Done when: Changing a password retains the previous value in history and it can be viewed. Search runs over decrypted content in memory and never writes a plaintext index to disk, verified by inspection. Attachments are encrypted with the vault and bounded in size. One-time code secrets generate correct codes against a known test vector. Do not build yet: No sync, autofill or sharing yet. ### Phase 4 · Sync that never picks a winner Build: Sync the encrypted file through storage the user controls, detecting divergence by version. On conflict keep both versions and merge per entry with the user deciding. Never overwrite a remote version you have not seen. Trap: Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. Done when: Two devices editing different entries offline merge without loss. Two devices editing the same entry produce a conflict with both values kept and shown. A remote version never seen locally is never overwritten, tested by simulating it. Sync failure leaves the local vault usable and unmodified. Do not build yet: No autofill, sharing or emergency access yet. ### Phase 5 · Getting credentials out safely Build: Copy to clipboard with a timed clear, and browser filling that matches on the registrable domain rather than on a substring. Export in an open format with an explicit warning that it is plaintext. Lock automatically on idle and on sleep. Trap: Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. Done when: Credential matching uses the registrable domain and refuses a lookalike address containing the real one as a substring, tested with a crafted example. The clipboard clears after a stated interval and clearing is verified. Export warns that the file is plaintext and the warning cannot be suppressed. The vault locks on idle and on system sleep. Do not build yet: Nothing after this. ### Out of scope (and why) - Designing any cryptography. Every primitive comes from a maintained library at published parameters. - A hosted sync service, which makes you responsible for other people's vaults and is a different risk entirely. - Sharing between users and emergency access, which are genuinely hard key management problems well beyond a single-user vault. - Breach monitoring, which requires either sending something derived from the password to a service or holding a very large dataset. ### README must contain - That no cryptography is designed here and every parameter is copied from published guidance, with the version recorded - That derivation parameters live in the vault file so an old vault still opens after the defaults change - That writes go to a temporary file, are verified readable, and only then replace the vault - That sync conflicts keep both sides rather than picking the most recent - That credential matching uses the registrable domain, because substring matching is how phishing works ===== AGENTS.md ===== # Agent instructions · Enpass indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A maintained library with published parameters, no custom constructions, Argon2id at the current published parameters, recorded per vault, A versioned file format written atomically, Through storage the user controls, with conflicts kept rather than resolved, A local application; the vault never leaves the machine unencrypted. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ===== BUILD_PLAN.md ===== # Build plan · Enpass A local encrypted vault whose two hard problems are handling keys correctly using primitives you did not invent, and never losing data across writes, sync conflicts and format changes over years of use. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A vault you can still open next year Encryption uses vetted primitives and the file records how to decrypt it. ### Steps 1. Use published parameters and record them Copied from the guidance, stored in the file, with the version noted. Files: `src/crypto.ts`, `docs/crypto-parameters.md` 2. Use an authenticated mode Tampering should fail loudly rather than decrypt to nonsense. Files: `src/crypto.ts` 3. Keep the master password in memory only Never to disk, never to a log, and zeroed when locked. Files: `src/session.ts`, `tests/no-secret-leak.test.ts` ### Done when - [ ] The file records version, algorithm and parameters - [ ] An old-parameter vault opens after defaults change - [ ] Tampering is detected rather than silently decrypting - [ ] The master password never reaches disk or logs ### Watch out - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. ## Phase 2 · Writes that cannot half happen A crash mid-save never leaves an unreadable vault. ### Steps 1. Write, verify, then replace Verify by opening the new file before it becomes the vault. Files: `src/storage.ts` 2. Keep previous generations The cheapest insurance against every failure in this category. Files: `src/backups.ts` 3. Test the restore in the suite A backup nobody has restored is a hope. Files: `tests/restore.test.ts` ### Done when - [ ] A crash mid-write leaves the previous vault intact - [ ] The new file is verified before replacing - [ ] Backups are automatic with a stated retention - [ ] Restore is exercised in the suite ### Watch out - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. ## Phase 3 · Entries, history and the awkward fields A vault holds real credentials including the ones that do not fit a template. ### Steps 1. Keep field history A changed password sometimes needs to be changed back. Files: `src/history.ts` 2. Search in memory, never index to disk A plaintext search index defeats the encryption entirely. Files: `src/search.ts`, `tests/no-plaintext-index.test.ts` 3. Verify code generation against test vectors Time-based codes are easy to get subtly wrong. Files: `src/otp.ts`, `src/otp.test.ts` ### Done when - [ ] Password history is retained and viewable - [ ] No plaintext index is written to disk - [ ] Attachments are encrypted and size bounded - [ ] One-time codes match known test vectors ### Watch out - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. ## Phase 4 · Sync that never picks a winner Two devices editing the vault do not lose an entry. ### Steps 1. Detect divergence by version Not by timestamp. Clocks disagree and the loser is a password. Files: `src/sync.ts` 2. Keep both sides of a conflict Merging at entry level with the user deciding. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Never overwrite an unseen version The remote may hold the only copy of something. Files: `src/sync.ts` ### Done when - [ ] Offline edits to different entries merge without loss - [ ] Same-entry conflicts keep both values - [ ] An unseen remote version is never overwritten - [ ] Sync failure leaves the local vault usable ### Watch out - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ## Not in this build - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. ## After v1, if you want it - Passkey storage, which is a different credential model and worth doing properly rather than quickly - A read-only view for a second device, which needs less key material than full access - Vault health reporting on reused and old credentials, computed entirely locally ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The encrypted vault. Everything you have, so back it up. VAULT_PATH=~/.local/share/vault/vault.enc # Required. Copied from current guidance and written into each vault it produces. KDF_PARAMS=argon2id:m=47104,t=1,p=1 # Required. Automatic generations. The cheapest insurance in this category. BACKUP_DIR=~/.local/share/vault/backups # Required. How many previous vaults to retain. Encrypted, so keeping several is safe. BACKUP_KEEP=10 # Required. After copying a credential. Verified to have cleared. CLIPBOARD_CLEAR_SECONDS=30 # Required. Also locks on sleep. An unlocked vault in memory is the exposure. LOCK_IDLE_MINUTES=5
You are building a lean indie version of Enpass. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Enpass · indie build A local encrypted vault whose two hard problems are handling keys correctly using primitives you did not invent, and never losing data across writes, sync conflicts and format changes over years of use. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Cryptography | A maintained library with published parameters, no custom constructions | designing this yourself is where password managers fail | | Key derivation | Argon2id at the current published parameters, recorded per vault | parameters change and an old vault must still open | | Storage | A versioned file format written atomically | a half-written vault is a lost vault | | Sync | Through storage the user controls, with conflicts kept rather than resolved | a merge that picks a winner loses a password silently | | Runtime | A local application; the vault never leaves the machine unencrypted | the whole premise is that nobody else can read it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A maintained cryptography library with Argon2id** · free - Why: Every primitive comes from here, and choosing an unmaintained or hand-rolled implementation is the single worst decision available in this project. - Get it: A well-reviewed library for your runtime providing Argon2id and an authenticated encryption mode. - Verify: It produces the published test vectors for both - [ ] **The current password storage guidance** · free - Why: The derivation parameters are copied from it rather than chosen, and they change, so the version you followed needs recording. - Get it: Read the current guidance, note the recommended parameters and the date, and store both in your documentation. - Verify: A written note records the parameters and the guidance version followed - [ ] **Storage you control for sync** (optional) · pennies per month, or free with existing sync - Why: The whole premise is that the vault syncs through the user's own storage rather than a service you operate, which changes the trust model entirely. - Get it: Any file sync you already use, or object storage with a key you hold. - Verify: A file written on one device appears on another - [ ] **Two machines you can take offline** · free - Why: Conflict handling is the part most likely to lose data and it is only testable with two devices editing offline simultaneously. - Get it: A second machine or a virtual machine with its own device identifier. - Verify: Both can edit offline and reconcile against the same remote ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. - mature apps, browser extensions, platform biometrics, and ongoing security maintenance - independent security audits - global relay infrastructure - breach monitoring data - account recovery and support If one of those is essential to you, that is the reason to keep paying for Enpass, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Enpass The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a password vault encrypted on your machine and synced through storage you own to replace Enpass. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and neither is negotiable. Do not design any cryptography. Use vetted primitives through a maintained library, in the modes their documentation specifies. For deriving the vault key from a master password that means a memory-hard function with published parameters: the current guidance is Argon2id at around forty six mebibytes with one iteration, or nineteen mebibytes with two, and if that is unavailable, scrypt or a very high iteration count with a suitable hash. Copy the parameters from the guidance rather than choosing your own, and record which version you followed. Second, a password manager that loses a vault is worse than not having one, because people put everything in it and stop remembering any of it. So the format is versioned from the first release, every write is atomic, backups are automatic and a restore is tested in the suite rather than assumed. More users lose a vault to a corrupted write or a failed sync than to an attacker. The corollary of both: if you cannot decrypt an old vault after a format change, or a half-written file leaves the vault unreadable, the cryptography being correct did not help. ### Stack (fixed, do not substitute) - Cryptography: A maintained library with published parameters, no custom constructions · designing this yourself is where password managers fail - Key derivation: Argon2id at the current published parameters, recorded per vault · parameters change and an old vault must still open - Storage: A versioned file format written atomically · a half-written vault is a lost vault - Sync: Through storage the user controls, with conflicts kept rather than resolved · a merge that picks a winner loses a password silently - Runtime: A local application; the vault never leaves the machine unencrypted · the whole premise is that nobody else can read it ### Data model (create this before Phase 1) - vault_file(format_version, kdf_name, kdf_params_json, salt, nonce, ciphertext, mac) · everything needed to open it later - entries(id, kind, title, fields_json, tags_json, created_at, updated_at, deleted_at) · inside the encrypted payload - history(entry_id, ordinal, fields_json, at) · previous values, because a changed password sometimes needs the old one - attachments(entry_id, id, filename, bytes, blob_ref) · encrypted with the vault, size bounded - sync_state(remote_ref, last_seen_version, last_pushed_version, device_id) - conflicts(id, remote_version, local_version, detected_at, resolved_at, resolution) · both sides retained - The key derivation parameters are stored in the file, so a vault written under old parameters still opens after the recommendations change. ### Phase 1 · A vault you can still open next year Build: Derive the vault key from the master password using a memory-hard function at the currently published parameters. Encrypt with an authenticated mode. Store the algorithm, parameters, salt and format version in the file itself. Trap: Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. Done when: The vault file records its format version, algorithm and derivation parameters, and a vault written under old parameters opens after the defaults change, tested directly. Encryption is authenticated so tampering is detected rather than producing garbage. No custom cryptographic construction exists anywhere, verified by review. The master password is never written to disk or logged. Do not build yet: No entries, sync, autofill or backup. ### Phase 2 · Writes that cannot half happen Build: Write to a temporary file, flush, then replace atomically. Keep the previous version until the new one is verified readable. Take automatic backups and test a restore in the suite. Trap: Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. Done when: A crash injected mid-write leaves the previous vault intact and openable, tested by killing the process during a save. The new file is verified readable before replacing the old one. Automatic backups are kept with a stated retention. A restore from backup is exercised in the test suite rather than documented. Do not build yet: No sync, autofill or sharing yet. ### Phase 3 · Entries, history and the awkward fields Build: Entries with typed fields, tags and search over the decrypted payload in memory. Keep previous field values as history. Support notes, attachments and one-time code secrets with a bounded attachment size. Trap: Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. Done when: Changing a password retains the previous value in history and it can be viewed. Search runs over decrypted content in memory and never writes a plaintext index to disk, verified by inspection. Attachments are encrypted with the vault and bounded in size. One-time code secrets generate correct codes against a known test vector. Do not build yet: No sync, autofill or sharing yet. ### Phase 4 · Sync that never picks a winner Build: Sync the encrypted file through storage the user controls, detecting divergence by version. On conflict keep both versions and merge per entry with the user deciding. Never overwrite a remote version you have not seen. Trap: Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. Done when: Two devices editing different entries offline merge without loss. Two devices editing the same entry produce a conflict with both values kept and shown. A remote version never seen locally is never overwritten, tested by simulating it. Sync failure leaves the local vault usable and unmodified. Do not build yet: No autofill, sharing or emergency access yet. ### Phase 5 · Getting credentials out safely Build: Copy to clipboard with a timed clear, and browser filling that matches on the registrable domain rather than on a substring. Export in an open format with an explicit warning that it is plaintext. Lock automatically on idle and on sleep. Trap: Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. Done when: Credential matching uses the registrable domain and refuses a lookalike address containing the real one as a substring, tested with a crafted example. The clipboard clears after a stated interval and clearing is verified. Export warns that the file is plaintext and the warning cannot be suppressed. The vault locks on idle and on system sleep. Do not build yet: Nothing after this. ### Out of scope (and why) - Designing any cryptography. Every primitive comes from a maintained library at published parameters. - A hosted sync service, which makes you responsible for other people's vaults and is a different risk entirely. - Sharing between users and emergency access, which are genuinely hard key management problems well beyond a single-user vault. - Breach monitoring, which requires either sending something derived from the password to a service or holding a very large dataset. ### README must contain - That no cryptography is designed here and every parameter is copied from published guidance, with the version recorded - That derivation parameters live in the vault file so an old vault still opens after the defaults change - That writes go to a temporary file, are verified readable, and only then replace the vault - That sync conflicts keep both sides rather than picking the most recent - That credential matching uses the registrable domain, because substring matching is how phishing works ===== AGENTS.md ===== # Agent instructions · Enpass indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A maintained library with published parameters, no custom constructions, Argon2id at the current published parameters, recorded per vault, A versioned file format written atomically, Through storage the user controls, with conflicts kept rather than resolved, A local application; the vault never leaves the machine unencrypted. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ===== BUILD_PLAN.md ===== # Build plan · Enpass A local encrypted vault whose two hard problems are handling keys correctly using primitives you did not invent, and never losing data across writes, sync conflicts and format changes over years of use. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A vault you can still open next year Encryption uses vetted primitives and the file records how to decrypt it. ### Steps 1. Use published parameters and record them Copied from the guidance, stored in the file, with the version noted. Files: `src/crypto.ts`, `docs/crypto-parameters.md` 2. Use an authenticated mode Tampering should fail loudly rather than decrypt to nonsense. Files: `src/crypto.ts` 3. Keep the master password in memory only Never to disk, never to a log, and zeroed when locked. Files: `src/session.ts`, `tests/no-secret-leak.test.ts` ### Done when - [ ] The file records version, algorithm and parameters - [ ] An old-parameter vault opens after defaults change - [ ] Tampering is detected rather than silently decrypting - [ ] The master password never reaches disk or logs ### Watch out - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. ## Phase 2 · Writes that cannot half happen A crash mid-save never leaves an unreadable vault. ### Steps 1. Write, verify, then replace Verify by opening the new file before it becomes the vault. Files: `src/storage.ts` 2. Keep previous generations The cheapest insurance against every failure in this category. Files: `src/backups.ts` 3. Test the restore in the suite A backup nobody has restored is a hope. Files: `tests/restore.test.ts` ### Done when - [ ] A crash mid-write leaves the previous vault intact - [ ] The new file is verified before replacing - [ ] Backups are automatic with a stated retention - [ ] Restore is exercised in the suite ### Watch out - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. ## Phase 3 · Entries, history and the awkward fields A vault holds real credentials including the ones that do not fit a template. ### Steps 1. Keep field history A changed password sometimes needs to be changed back. Files: `src/history.ts` 2. Search in memory, never index to disk A plaintext search index defeats the encryption entirely. Files: `src/search.ts`, `tests/no-plaintext-index.test.ts` 3. Verify code generation against test vectors Time-based codes are easy to get subtly wrong. Files: `src/otp.ts`, `src/otp.test.ts` ### Done when - [ ] Password history is retained and viewable - [ ] No plaintext index is written to disk - [ ] Attachments are encrypted and size bounded - [ ] One-time codes match known test vectors ### Watch out - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. ## Phase 4 · Sync that never picks a winner Two devices editing the vault do not lose an entry. ### Steps 1. Detect divergence by version Not by timestamp. Clocks disagree and the loser is a password. Files: `src/sync.ts` 2. Keep both sides of a conflict Merging at entry level with the user deciding. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Never overwrite an unseen version The remote may hold the only copy of something. Files: `src/sync.ts` ### Done when - [ ] Offline edits to different entries merge without loss - [ ] Same-entry conflicts keep both values - [ ] An unseen remote version is never overwritten - [ ] Sync failure leaves the local vault usable ### Watch out - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ## Not in this build - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. ## After v1, if you want it - Passkey storage, which is a different credential model and worth doing properly rather than quickly - A read-only view for a second device, which needs less key material than full access - Vault health reporting on reused and old credentials, computed entirely locally ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The encrypted vault. Everything you have, so back it up. VAULT_PATH=~/.local/share/vault/vault.enc # Required. Copied from current guidance and written into each vault it produces. KDF_PARAMS=argon2id:m=47104,t=1,p=1 # Required. Automatic generations. The cheapest insurance in this category. BACKUP_DIR=~/.local/share/vault/backups # Required. How many previous vaults to retain. Encrypted, so keeping several is safe. BACKUP_KEEP=10 # Required. After copying a credential. Verified to have cleared. CLIPBOARD_CLEAR_SECONDS=30 # Required. Also locks on sleep. An unlocked vault in memory is the exposure. LOCK_IDLE_MINUTES=5
You are building a production product version of Enpass. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Enpass · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Enpass, store an encrypted vault locally and optionally sync it through the user's own cloud folder. The hard boundary is mature apps, browser extensions, platform biometrics, and ongoing security maintenance, plus security assurance, infrastructure, and trust. ## Product outcome A vault that uses cryptography nobody here invented, cannot be corrupted by a crash mid-save, never silently loses an entry to a sync conflict, and will not hand a password to a lookalike domain. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop OS - secure backup location - modern cryptographic libraries - careful review before real-world use ## Explicit non-goals for v1 - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. - mature apps, browser extensions, platform biometrics, and ongoing security maintenance - independent security audits - global relay infrastructure - breach monitoring data - account recovery and support ## Success criteria - An old-parameter vault is confirmed to open after defaults change - A crash mid-write is confirmed to leave the previous vault intact - No plaintext index is confirmed written to disk - A same-entry sync conflict is confirmed to keep both values - A lookalike domain is confirmed refused ===== BRIEF.md ===== # Build brief · Enpass The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a password vault encrypted on your machine and synced through storage you own to replace Enpass. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and neither is negotiable. Do not design any cryptography. Use vetted primitives through a maintained library, in the modes their documentation specifies. For deriving the vault key from a master password that means a memory-hard function with published parameters: the current guidance is Argon2id at around forty six mebibytes with one iteration, or nineteen mebibytes with two, and if that is unavailable, scrypt or a very high iteration count with a suitable hash. Copy the parameters from the guidance rather than choosing your own, and record which version you followed. Second, a password manager that loses a vault is worse than not having one, because people put everything in it and stop remembering any of it. So the format is versioned from the first release, every write is atomic, backups are automatic and a restore is tested in the suite rather than assumed. More users lose a vault to a corrupted write or a failed sync than to an attacker. The corollary of both: if you cannot decrypt an old vault after a format change, or a half-written file leaves the vault unreadable, the cryptography being correct did not help. ### Stack (fixed, do not substitute) - Cryptography: A maintained library with published parameters, no custom constructions · designing this yourself is where password managers fail - Key derivation: Argon2id at the current published parameters, recorded per vault · parameters change and an old vault must still open - Storage: A versioned file format written atomically · a half-written vault is a lost vault - Sync: Through storage the user controls, with conflicts kept rather than resolved · a merge that picks a winner loses a password silently - Runtime: A local application; the vault never leaves the machine unencrypted · the whole premise is that nobody else can read it ### Data model (create this before Phase 1) - vault_file(format_version, kdf_name, kdf_params_json, salt, nonce, ciphertext, mac) · everything needed to open it later - entries(id, kind, title, fields_json, tags_json, created_at, updated_at, deleted_at) · inside the encrypted payload - history(entry_id, ordinal, fields_json, at) · previous values, because a changed password sometimes needs the old one - attachments(entry_id, id, filename, bytes, blob_ref) · encrypted with the vault, size bounded - sync_state(remote_ref, last_seen_version, last_pushed_version, device_id) - conflicts(id, remote_version, local_version, detected_at, resolved_at, resolution) · both sides retained - The key derivation parameters are stored in the file, so a vault written under old parameters still opens after the recommendations change. ### Phase 1 · A vault you can still open next year Build: Derive the vault key from the master password using a memory-hard function at the currently published parameters. Encrypt with an authenticated mode. Store the algorithm, parameters, salt and format version in the file itself. Trap: Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. Done when: The vault file records its format version, algorithm and derivation parameters, and a vault written under old parameters opens after the defaults change, tested directly. Encryption is authenticated so tampering is detected rather than producing garbage. No custom cryptographic construction exists anywhere, verified by review. The master password is never written to disk or logged. Do not build yet: No entries, sync, autofill or backup. ### Phase 2 · Writes that cannot half happen Build: Write to a temporary file, flush, then replace atomically. Keep the previous version until the new one is verified readable. Take automatic backups and test a restore in the suite. Trap: Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. Done when: A crash injected mid-write leaves the previous vault intact and openable, tested by killing the process during a save. The new file is verified readable before replacing the old one. Automatic backups are kept with a stated retention. A restore from backup is exercised in the test suite rather than documented. Do not build yet: No sync, autofill or sharing yet. ### Phase 3 · Entries, history and the awkward fields Build: Entries with typed fields, tags and search over the decrypted payload in memory. Keep previous field values as history. Support notes, attachments and one-time code secrets with a bounded attachment size. Trap: Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. Done when: Changing a password retains the previous value in history and it can be viewed. Search runs over decrypted content in memory and never writes a plaintext index to disk, verified by inspection. Attachments are encrypted with the vault and bounded in size. One-time code secrets generate correct codes against a known test vector. Do not build yet: No sync, autofill or sharing yet. ### Phase 4 · Sync that never picks a winner Build: Sync the encrypted file through storage the user controls, detecting divergence by version. On conflict keep both versions and merge per entry with the user deciding. Never overwrite a remote version you have not seen. Trap: Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. Done when: Two devices editing different entries offline merge without loss. Two devices editing the same entry produce a conflict with both values kept and shown. A remote version never seen locally is never overwritten, tested by simulating it. Sync failure leaves the local vault usable and unmodified. Do not build yet: No autofill, sharing or emergency access yet. ### Phase 5 · Getting credentials out safely Build: Copy to clipboard with a timed clear, and browser filling that matches on the registrable domain rather than on a substring. Export in an open format with an explicit warning that it is plaintext. Lock automatically on idle and on sleep. Trap: Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. Done when: Credential matching uses the registrable domain and refuses a lookalike address containing the real one as a substring, tested with a crafted example. The clipboard clears after a stated interval and clearing is verified. Export warns that the file is plaintext and the warning cannot be suppressed. The vault locks on idle and on system sleep. Do not build yet: Nothing after this. ### Out of scope (and why) - Designing any cryptography. Every primitive comes from a maintained library at published parameters. - A hosted sync service, which makes you responsible for other people's vaults and is a different risk entirely. - Sharing between users and emergency access, which are genuinely hard key management problems well beyond a single-user vault. - Breach monitoring, which requires either sending something derived from the password to a service or holding a very large dataset. ### README must contain - That no cryptography is designed here and every parameter is copied from published guidance, with the version recorded - That derivation parameters live in the vault file so an old vault still opens after the defaults change - That writes go to a temporary file, are verified readable, and only then replace the vault - That sync conflicts keep both sides rather than picking the most recent - That credential matching uses the registrable domain, because substring matching is how phishing works ===== ARCHITECTURE.md ===== # Architecture · Enpass ## Stack | Part | Choice | Why | | --- | --- | --- | | Cryptography | A maintained library with published parameters, no custom constructions | designing this yourself is where password managers fail | | Key derivation | Argon2id at the current published parameters, recorded per vault | parameters change and an old vault must still open | | Storage | A versioned file format written atomically | a half-written vault is a lost vault | | Sync | Through storage the user controls, with conflicts kept rather than resolved | a merge that picks a winner loses a password silently | | Runtime | A local application; the vault never leaves the machine unencrypted | the whole premise is that nobody else can read it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Cryptography | Vetted primitives at published parameters. | Any maintained library. Never a custom construction, whatever the reason. | | Storage | Atomic verified writes and generations. | Any implementation, provided the new file is verified before replacing. | | Entries | Fields, history and in-memory search. | Any store, provided no plaintext index reaches disk. | | Sync | Divergence detection and retained conflicts. | Any transport, provided conflicts keep both sides. | | Matching | Registrable domain comparison. | Not swappable for substring matching, which is the phishing vector. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VAULT_PATH` · required · The encrypted vault. Everything you have, so back it up. - `KDF_PARAMS` · required · Copied from current guidance and written into each vault it produces. - `BACKUP_DIR` · required · Automatic generations. The cheapest insurance in this category. - `BACKUP_KEEP` · required · How many previous vaults to retain. Encrypted, so keeping several is safe. - `CLIPBOARD_CLEAR_SECONDS` · required · After copying a credential. Verified to have cleared. - `LOCK_IDLE_MINUTES` · required · Also locks on sleep. An unlocked vault in memory is the exposure. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Enpass product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A maintained library with published parameters, no custom constructions, Argon2id at the current published parameters, recorded per vault, A versioned file format written atomically, Through storage the user controls, with conflicts kept rather than resolved, A local application; the vault never leaves the machine unencrypted. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. - Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. ===== MILESTONES.md ===== # Delivery milestones · Enpass Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A vault you can still open next year Encryption uses vetted primitives and the file records how to decrypt it. ### Steps 1. Use published parameters and record them Copied from the guidance, stored in the file, with the version noted. Files: `src/crypto.ts`, `docs/crypto-parameters.md` 2. Use an authenticated mode Tampering should fail loudly rather than decrypt to nonsense. Files: `src/crypto.ts` 3. Keep the master password in memory only Never to disk, never to a log, and zeroed when locked. Files: `src/session.ts`, `tests/no-secret-leak.test.ts` ### Done when - [ ] The file records version, algorithm and parameters - [ ] An old-parameter vault opens after defaults change - [ ] Tampering is detected rather than silently decrypting - [ ] The master password never reaches disk or logs ### Watch out - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. ## M2 · Writes that cannot half happen A crash mid-save never leaves an unreadable vault. ### Steps 1. Write, verify, then replace Verify by opening the new file before it becomes the vault. Files: `src/storage.ts` 2. Keep previous generations The cheapest insurance against every failure in this category. Files: `src/backups.ts` 3. Test the restore in the suite A backup nobody has restored is a hope. Files: `tests/restore.test.ts` ### Done when - [ ] A crash mid-write leaves the previous vault intact - [ ] The new file is verified before replacing - [ ] Backups are automatic with a stated retention - [ ] Restore is exercised in the suite ### Watch out - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. ## M3 · Entries, history and the awkward fields A vault holds real credentials including the ones that do not fit a template. ### Steps 1. Keep field history A changed password sometimes needs to be changed back. Files: `src/history.ts` 2. Search in memory, never index to disk A plaintext search index defeats the encryption entirely. Files: `src/search.ts`, `tests/no-plaintext-index.test.ts` 3. Verify code generation against test vectors Time-based codes are easy to get subtly wrong. Files: `src/otp.ts`, `src/otp.test.ts` ### Done when - [ ] Password history is retained and viewable - [ ] No plaintext index is written to disk - [ ] Attachments are encrypted and size bounded - [ ] One-time codes match known test vectors ### Watch out - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. ## M4 · Sync that never picks a winner Two devices editing the vault do not lose an entry. ### Steps 1. Detect divergence by version Not by timestamp. Clocks disagree and the loser is a password. Files: `src/sync.ts` 2. Keep both sides of a conflict Merging at entry level with the user deciding. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Never overwrite an unseen version The remote may hold the only copy of something. Files: `src/sync.ts` ### Done when - [ ] Offline edits to different entries merge without loss - [ ] Same-entry conflicts keep both values - [ ] An unseen remote version is never overwritten - [ ] Sync failure leaves the local vault usable ### Watch out - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ## M5 · Getting credentials out safely (production only) Filling and exporting work without leaving secrets somewhere unexpected. ### Steps 1. Match on the registrable domain Substring matching is how a phishing page gets your bank password. Files: `src/matching.ts`, `tests/phishing.test.ts` 2. Clear the clipboard on a timer And verify it cleared rather than assuming. Files: `src/clipboard.ts` 3. Lock on idle and on sleep A closed laptop should not have an unlocked vault in memory. Files: `src/lock.ts` ### Done when - [ ] A lookalike domain is refused - [ ] The clipboard clears and the clearing is verified - [ ] Plaintext export warns unsuppressibly - [ ] The vault locks on idle and sleep ### Watch out - Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. ===== OPERATIONS.md ===== # Operations · Enpass ## Backup This is the one build where backup is the primary feature rather than an operational concern. Automatic generations, offsite copies, and a restore exercised in the suite, because losing a vault loses everything the user stopped remembering. ## Restore Restore and confirm the vault opens with the master password and that its recorded derivation parameters were used rather than the current defaults. A restore that cannot open the file is the failure this build exists to prevent. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: no plaintext index, no secret in a log, an old-parameter vault still opening, and a crash mid-write leaving the previous vault intact. ## Incident checklist An unopenable vault is the incident, and it is far more likely to come from a truncated write or a format change than from an attacker. The generations and the recorded parameters are what make recovery possible rather than theoretical. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An old-parameter vault is confirmed to open after defaults change - [ ] A crash mid-write is confirmed to leave the previous vault intact - [ ] No plaintext index is confirmed written to disk - [ ] A same-entry sync conflict is confirmed to keep both values - [ ] A lookalike domain is confirmed refused ## Launch constraint Do not market omitted Enpass capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The encrypted vault. Everything you have, so back it up. VAULT_PATH=~/.local/share/vault/vault.enc # Required. Copied from current guidance and written into each vault it produces. KDF_PARAMS=argon2id:m=47104,t=1,p=1 # Required. Automatic generations. The cheapest insurance in this category. BACKUP_DIR=~/.local/share/vault/backups # Required. How many previous vaults to retain. Encrypted, so keeping several is safe. BACKUP_KEEP=10 # Required. After copying a credential. Verified to have cleared. CLIPBOARD_CLEAR_SECONDS=30 # Required. Also locks on sleep. An unlocked vault in memory is the exposure. LOCK_IDLE_MINUTES=5
# Enpass · indie build A local encrypted vault whose two hard problems are handling keys correctly using primitives you did not invent, and never losing data across writes, sync conflicts and format changes over years of use. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Cryptography | A maintained library with published parameters, no custom constructions | designing this yourself is where password managers fail | | Key derivation | Argon2id at the current published parameters, recorded per vault | parameters change and an old vault must still open | | Storage | A versioned file format written atomically | a half-written vault is a lost vault | | Sync | Through storage the user controls, with conflicts kept rather than resolved | a merge that picks a winner loses a password silently | | Runtime | A local application; the vault never leaves the machine unencrypted | the whole premise is that nobody else can read it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A maintained cryptography library with Argon2id** · free - Why: Every primitive comes from here, and choosing an unmaintained or hand-rolled implementation is the single worst decision available in this project. - Get it: A well-reviewed library for your runtime providing Argon2id and an authenticated encryption mode. - Verify: It produces the published test vectors for both - [ ] **The current password storage guidance** · free - Why: The derivation parameters are copied from it rather than chosen, and they change, so the version you followed needs recording. - Get it: Read the current guidance, note the recommended parameters and the date, and store both in your documentation. - Verify: A written note records the parameters and the guidance version followed - [ ] **Storage you control for sync** (optional) · pennies per month, or free with existing sync - Why: The whole premise is that the vault syncs through the user's own storage rather than a service you operate, which changes the trust model entirely. - Get it: Any file sync you already use, or object storage with a key you hold. - Verify: A file written on one device appears on another - [ ] **Two machines you can take offline** · free - Why: Conflict handling is the part most likely to lose data and it is only testable with two devices editing offline simultaneously. - Get it: A second machine or a virtual machine with its own device identifier. - Verify: Both can edit offline and reconcile against the same remote ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. - mature apps, browser extensions, platform biometrics, and ongoing security maintenance - independent security audits - global relay infrastructure - breach monitoring data - account recovery and support If one of those is essential to you, that is the reason to keep paying for Enpass, and the README should say so rather than pretend.
# Build brief · Enpass The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a password vault encrypted on your machine and synced through storage you own to replace Enpass. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and neither is negotiable. Do not design any cryptography. Use vetted primitives through a maintained library, in the modes their documentation specifies. For deriving the vault key from a master password that means a memory-hard function with published parameters: the current guidance is Argon2id at around forty six mebibytes with one iteration, or nineteen mebibytes with two, and if that is unavailable, scrypt or a very high iteration count with a suitable hash. Copy the parameters from the guidance rather than choosing your own, and record which version you followed. Second, a password manager that loses a vault is worse than not having one, because people put everything in it and stop remembering any of it. So the format is versioned from the first release, every write is atomic, backups are automatic and a restore is tested in the suite rather than assumed. More users lose a vault to a corrupted write or a failed sync than to an attacker. The corollary of both: if you cannot decrypt an old vault after a format change, or a half-written file leaves the vault unreadable, the cryptography being correct did not help. ### Stack (fixed, do not substitute) - Cryptography: A maintained library with published parameters, no custom constructions · designing this yourself is where password managers fail - Key derivation: Argon2id at the current published parameters, recorded per vault · parameters change and an old vault must still open - Storage: A versioned file format written atomically · a half-written vault is a lost vault - Sync: Through storage the user controls, with conflicts kept rather than resolved · a merge that picks a winner loses a password silently - Runtime: A local application; the vault never leaves the machine unencrypted · the whole premise is that nobody else can read it ### Data model (create this before Phase 1) - vault_file(format_version, kdf_name, kdf_params_json, salt, nonce, ciphertext, mac) · everything needed to open it later - entries(id, kind, title, fields_json, tags_json, created_at, updated_at, deleted_at) · inside the encrypted payload - history(entry_id, ordinal, fields_json, at) · previous values, because a changed password sometimes needs the old one - attachments(entry_id, id, filename, bytes, blob_ref) · encrypted with the vault, size bounded - sync_state(remote_ref, last_seen_version, last_pushed_version, device_id) - conflicts(id, remote_version, local_version, detected_at, resolved_at, resolution) · both sides retained - The key derivation parameters are stored in the file, so a vault written under old parameters still opens after the recommendations change. ### Phase 1 · A vault you can still open next year Build: Derive the vault key from the master password using a memory-hard function at the currently published parameters. Encrypt with an authenticated mode. Store the algorithm, parameters, salt and format version in the file itself. Trap: Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. Done when: The vault file records its format version, algorithm and derivation parameters, and a vault written under old parameters opens after the defaults change, tested directly. Encryption is authenticated so tampering is detected rather than producing garbage. No custom cryptographic construction exists anywhere, verified by review. The master password is never written to disk or logged. Do not build yet: No entries, sync, autofill or backup. ### Phase 2 · Writes that cannot half happen Build: Write to a temporary file, flush, then replace atomically. Keep the previous version until the new one is verified readable. Take automatic backups and test a restore in the suite. Trap: Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. Done when: A crash injected mid-write leaves the previous vault intact and openable, tested by killing the process during a save. The new file is verified readable before replacing the old one. Automatic backups are kept with a stated retention. A restore from backup is exercised in the test suite rather than documented. Do not build yet: No sync, autofill or sharing yet. ### Phase 3 · Entries, history and the awkward fields Build: Entries with typed fields, tags and search over the decrypted payload in memory. Keep previous field values as history. Support notes, attachments and one-time code secrets with a bounded attachment size. Trap: Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. Done when: Changing a password retains the previous value in history and it can be viewed. Search runs over decrypted content in memory and never writes a plaintext index to disk, verified by inspection. Attachments are encrypted with the vault and bounded in size. One-time code secrets generate correct codes against a known test vector. Do not build yet: No sync, autofill or sharing yet. ### Phase 4 · Sync that never picks a winner Build: Sync the encrypted file through storage the user controls, detecting divergence by version. On conflict keep both versions and merge per entry with the user deciding. Never overwrite a remote version you have not seen. Trap: Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. Done when: Two devices editing different entries offline merge without loss. Two devices editing the same entry produce a conflict with both values kept and shown. A remote version never seen locally is never overwritten, tested by simulating it. Sync failure leaves the local vault usable and unmodified. Do not build yet: No autofill, sharing or emergency access yet. ### Phase 5 · Getting credentials out safely Build: Copy to clipboard with a timed clear, and browser filling that matches on the registrable domain rather than on a substring. Export in an open format with an explicit warning that it is plaintext. Lock automatically on idle and on sleep. Trap: Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. Done when: Credential matching uses the registrable domain and refuses a lookalike address containing the real one as a substring, tested with a crafted example. The clipboard clears after a stated interval and clearing is verified. Export warns that the file is plaintext and the warning cannot be suppressed. The vault locks on idle and on system sleep. Do not build yet: Nothing after this. ### Out of scope (and why) - Designing any cryptography. Every primitive comes from a maintained library at published parameters. - A hosted sync service, which makes you responsible for other people's vaults and is a different risk entirely. - Sharing between users and emergency access, which are genuinely hard key management problems well beyond a single-user vault. - Breach monitoring, which requires either sending something derived from the password to a service or holding a very large dataset. ### README must contain - That no cryptography is designed here and every parameter is copied from published guidance, with the version recorded - That derivation parameters live in the vault file so an old vault still opens after the defaults change - That writes go to a temporary file, are verified readable, and only then replace the vault - That sync conflicts keep both sides rather than picking the most recent - That credential matching uses the registrable domain, because substring matching is how phishing works
# Agent instructions · Enpass indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A maintained library with published parameters, no custom constructions, Argon2id at the current published parameters, recorded per vault, A versioned file format written atomically, Through storage the user controls, with conflicts kept rather than resolved, A local application; the vault never leaves the machine unencrypted. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing.
# Build plan · Enpass A local encrypted vault whose two hard problems are handling keys correctly using primitives you did not invent, and never losing data across writes, sync conflicts and format changes over years of use. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A vault you can still open next year Encryption uses vetted primitives and the file records how to decrypt it. ### Steps 1. Use published parameters and record them Copied from the guidance, stored in the file, with the version noted. Files: `src/crypto.ts`, `docs/crypto-parameters.md` 2. Use an authenticated mode Tampering should fail loudly rather than decrypt to nonsense. Files: `src/crypto.ts` 3. Keep the master password in memory only Never to disk, never to a log, and zeroed when locked. Files: `src/session.ts`, `tests/no-secret-leak.test.ts` ### Done when - [ ] The file records version, algorithm and parameters - [ ] An old-parameter vault opens after defaults change - [ ] Tampering is detected rather than silently decrypting - [ ] The master password never reaches disk or logs ### Watch out - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. ## Phase 2 · Writes that cannot half happen A crash mid-save never leaves an unreadable vault. ### Steps 1. Write, verify, then replace Verify by opening the new file before it becomes the vault. Files: `src/storage.ts` 2. Keep previous generations The cheapest insurance against every failure in this category. Files: `src/backups.ts` 3. Test the restore in the suite A backup nobody has restored is a hope. Files: `tests/restore.test.ts` ### Done when - [ ] A crash mid-write leaves the previous vault intact - [ ] The new file is verified before replacing - [ ] Backups are automatic with a stated retention - [ ] Restore is exercised in the suite ### Watch out - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. ## Phase 3 · Entries, history and the awkward fields A vault holds real credentials including the ones that do not fit a template. ### Steps 1. Keep field history A changed password sometimes needs to be changed back. Files: `src/history.ts` 2. Search in memory, never index to disk A plaintext search index defeats the encryption entirely. Files: `src/search.ts`, `tests/no-plaintext-index.test.ts` 3. Verify code generation against test vectors Time-based codes are easy to get subtly wrong. Files: `src/otp.ts`, `src/otp.test.ts` ### Done when - [ ] Password history is retained and viewable - [ ] No plaintext index is written to disk - [ ] Attachments are encrypted and size bounded - [ ] One-time codes match known test vectors ### Watch out - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. ## Phase 4 · Sync that never picks a winner Two devices editing the vault do not lose an entry. ### Steps 1. Detect divergence by version Not by timestamp. Clocks disagree and the loser is a password. Files: `src/sync.ts` 2. Keep both sides of a conflict Merging at entry level with the user deciding. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Never overwrite an unseen version The remote may hold the only copy of something. Files: `src/sync.ts` ### Done when - [ ] Offline edits to different entries merge without loss - [ ] Same-entry conflicts keep both values - [ ] An unseen remote version is never overwritten - [ ] Sync failure leaves the local vault usable ### Watch out - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ## Not in this build - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. ## After v1, if you want it - Passkey storage, which is a different credential model and worth doing properly rather than quickly - A read-only view for a second device, which needs less key material than full access - Vault health reporting on reused and old credentials, computed entirely locally
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The encrypted vault. Everything you have, so back it up. VAULT_PATH=~/.local/share/vault/vault.enc # Required. Copied from current guidance and written into each vault it produces. KDF_PARAMS=argon2id:m=47104,t=1,p=1 # Required. Automatic generations. The cheapest insurance in this category. BACKUP_DIR=~/.local/share/vault/backups # Required. How many previous vaults to retain. Encrypted, so keeping several is safe. BACKUP_KEEP=10 # Required. After copying a credential. Verified to have cleared. CLIPBOARD_CLEAR_SECONDS=30 # Required. Also locks on sleep. An unlocked vault in memory is the exposure. LOCK_IDLE_MINUTES=5
# Enpass · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Enpass, store an encrypted vault locally and optionally sync it through the user's own cloud folder. The hard boundary is mature apps, browser extensions, platform biometrics, and ongoing security maintenance, plus security assurance, infrastructure, and trust. ## Product outcome A vault that uses cryptography nobody here invented, cannot be corrupted by a crash mid-save, never silently loses an entry to a sync conflict, and will not hand a password to a lookalike domain. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop OS - secure backup location - modern cryptographic libraries - careful review before real-world use ## Explicit non-goals for v1 - Any cryptography of your own design. - A hosted sync service. - Sharing between users and emergency access. - Breach monitoring. - mature apps, browser extensions, platform biometrics, and ongoing security maintenance - independent security audits - global relay infrastructure - breach monitoring data - account recovery and support ## Success criteria - An old-parameter vault is confirmed to open after defaults change - A crash mid-write is confirmed to leave the previous vault intact - No plaintext index is confirmed written to disk - A same-entry sync conflict is confirmed to keep both values - A lookalike domain is confirmed refused
# Build brief · Enpass The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a password vault encrypted on your machine and synced through storage you own to replace Enpass. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and neither is negotiable. Do not design any cryptography. Use vetted primitives through a maintained library, in the modes their documentation specifies. For deriving the vault key from a master password that means a memory-hard function with published parameters: the current guidance is Argon2id at around forty six mebibytes with one iteration, or nineteen mebibytes with two, and if that is unavailable, scrypt or a very high iteration count with a suitable hash. Copy the parameters from the guidance rather than choosing your own, and record which version you followed. Second, a password manager that loses a vault is worse than not having one, because people put everything in it and stop remembering any of it. So the format is versioned from the first release, every write is atomic, backups are automatic and a restore is tested in the suite rather than assumed. More users lose a vault to a corrupted write or a failed sync than to an attacker. The corollary of both: if you cannot decrypt an old vault after a format change, or a half-written file leaves the vault unreadable, the cryptography being correct did not help. ### Stack (fixed, do not substitute) - Cryptography: A maintained library with published parameters, no custom constructions · designing this yourself is where password managers fail - Key derivation: Argon2id at the current published parameters, recorded per vault · parameters change and an old vault must still open - Storage: A versioned file format written atomically · a half-written vault is a lost vault - Sync: Through storage the user controls, with conflicts kept rather than resolved · a merge that picks a winner loses a password silently - Runtime: A local application; the vault never leaves the machine unencrypted · the whole premise is that nobody else can read it ### Data model (create this before Phase 1) - vault_file(format_version, kdf_name, kdf_params_json, salt, nonce, ciphertext, mac) · everything needed to open it later - entries(id, kind, title, fields_json, tags_json, created_at, updated_at, deleted_at) · inside the encrypted payload - history(entry_id, ordinal, fields_json, at) · previous values, because a changed password sometimes needs the old one - attachments(entry_id, id, filename, bytes, blob_ref) · encrypted with the vault, size bounded - sync_state(remote_ref, last_seen_version, last_pushed_version, device_id) - conflicts(id, remote_version, local_version, detected_at, resolved_at, resolution) · both sides retained - The key derivation parameters are stored in the file, so a vault written under old parameters still opens after the recommendations change. ### Phase 1 · A vault you can still open next year Build: Derive the vault key from the master password using a memory-hard function at the currently published parameters. Encrypt with an authenticated mode. Store the algorithm, parameters, salt and format version in the file itself. Trap: Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. Done when: The vault file records its format version, algorithm and derivation parameters, and a vault written under old parameters opens after the defaults change, tested directly. Encryption is authenticated so tampering is detected rather than producing garbage. No custom cryptographic construction exists anywhere, verified by review. The master password is never written to disk or logged. Do not build yet: No entries, sync, autofill or backup. ### Phase 2 · Writes that cannot half happen Build: Write to a temporary file, flush, then replace atomically. Keep the previous version until the new one is verified readable. Take automatic backups and test a restore in the suite. Trap: Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. Done when: A crash injected mid-write leaves the previous vault intact and openable, tested by killing the process during a save. The new file is verified readable before replacing the old one. Automatic backups are kept with a stated retention. A restore from backup is exercised in the test suite rather than documented. Do not build yet: No sync, autofill or sharing yet. ### Phase 3 · Entries, history and the awkward fields Build: Entries with typed fields, tags and search over the decrypted payload in memory. Keep previous field values as history. Support notes, attachments and one-time code secrets with a bounded attachment size. Trap: Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. Done when: Changing a password retains the previous value in history and it can be viewed. Search runs over decrypted content in memory and never writes a plaintext index to disk, verified by inspection. Attachments are encrypted with the vault and bounded in size. One-time code secrets generate correct codes against a known test vector. Do not build yet: No sync, autofill or sharing yet. ### Phase 4 · Sync that never picks a winner Build: Sync the encrypted file through storage the user controls, detecting divergence by version. On conflict keep both versions and merge per entry with the user deciding. Never overwrite a remote version you have not seen. Trap: Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. Done when: Two devices editing different entries offline merge without loss. Two devices editing the same entry produce a conflict with both values kept and shown. A remote version never seen locally is never overwritten, tested by simulating it. Sync failure leaves the local vault usable and unmodified. Do not build yet: No autofill, sharing or emergency access yet. ### Phase 5 · Getting credentials out safely Build: Copy to clipboard with a timed clear, and browser filling that matches on the registrable domain rather than on a substring. Export in an open format with an explicit warning that it is plaintext. Lock automatically on idle and on sleep. Trap: Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring. Done when: Credential matching uses the registrable domain and refuses a lookalike address containing the real one as a substring, tested with a crafted example. The clipboard clears after a stated interval and clearing is verified. Export warns that the file is plaintext and the warning cannot be suppressed. The vault locks on idle and on system sleep. Do not build yet: Nothing after this. ### Out of scope (and why) - Designing any cryptography. Every primitive comes from a maintained library at published parameters. - A hosted sync service, which makes you responsible for other people's vaults and is a different risk entirely. - Sharing between users and emergency access, which are genuinely hard key management problems well beyond a single-user vault. - Breach monitoring, which requires either sending something derived from the password to a service or holding a very large dataset. ### README must contain - That no cryptography is designed here and every parameter is copied from published guidance, with the version recorded - That derivation parameters live in the vault file so an old vault still opens after the defaults change - That writes go to a temporary file, are verified readable, and only then replace the vault - That sync conflicts keep both sides rather than picking the most recent - That credential matching uses the registrable domain, because substring matching is how phishing works
# Architecture · Enpass ## Stack | Part | Choice | Why | | --- | --- | --- | | Cryptography | A maintained library with published parameters, no custom constructions | designing this yourself is where password managers fail | | Key derivation | Argon2id at the current published parameters, recorded per vault | parameters change and an old vault must still open | | Storage | A versioned file format written atomically | a half-written vault is a lost vault | | Sync | Through storage the user controls, with conflicts kept rather than resolved | a merge that picks a winner loses a password silently | | Runtime | A local application; the vault never leaves the machine unencrypted | the whole premise is that nobody else can read it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Cryptography | Vetted primitives at published parameters. | Any maintained library. Never a custom construction, whatever the reason. | | Storage | Atomic verified writes and generations. | Any implementation, provided the new file is verified before replacing. | | Entries | Fields, history and in-memory search. | Any store, provided no plaintext index reaches disk. | | Sync | Divergence detection and retained conflicts. | Any transport, provided conflicts keep both sides. | | Matching | Registrable domain comparison. | Not swappable for substring matching, which is the phishing vector. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VAULT_PATH` · required · The encrypted vault. Everything you have, so back it up. - `KDF_PARAMS` · required · Copied from current guidance and written into each vault it produces. - `BACKUP_DIR` · required · Automatic generations. The cheapest insurance in this category. - `BACKUP_KEEP` · required · How many previous vaults to retain. Encrypted, so keeping several is safe. - `CLIPBOARD_CLEAR_SECONDS` · required · After copying a credential. Verified to have cleared. - `LOCK_IDLE_MINUTES` · required · Also locks on sleep. An unlocked vault in memory is the exposure. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Enpass product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A maintained library with published parameters, no custom constructions, Argon2id at the current published parameters, recorded per vault, A versioned file format written atomically, Through storage the user controls, with conflicts kept rather than resolved, A local application; the vault never leaves the machine unencrypted. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. - Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring.
# Delivery milestones · Enpass Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A vault you can still open next year Encryption uses vetted primitives and the file records how to decrypt it. ### Steps 1. Use published parameters and record them Copied from the guidance, stored in the file, with the version noted. Files: `src/crypto.ts`, `docs/crypto-parameters.md` 2. Use an authenticated mode Tampering should fail loudly rather than decrypt to nonsense. Files: `src/crypto.ts` 3. Keep the master password in memory only Never to disk, never to a log, and zeroed when locked. Files: `src/session.ts`, `tests/no-secret-leak.test.ts` ### Done when - [ ] The file records version, algorithm and parameters - [ ] An old-parameter vault opens after defaults change - [ ] Tampering is detected rather than silently decrypting - [ ] The master password never reaches disk or logs ### Watch out - Hard-coding the derivation parameters means a vault written today cannot be opened once you raise them, unless you also keep every past value in code forever. Store the parameters in the file, so opening reads what was used and upgrading rewrites with the new ones on the next unlock. ## M2 · Writes that cannot half happen A crash mid-save never leaves an unreadable vault. ### Steps 1. Write, verify, then replace Verify by opening the new file before it becomes the vault. Files: `src/storage.ts` 2. Keep previous generations The cheapest insurance against every failure in this category. Files: `src/backups.ts` 3. Test the restore in the suite A backup nobody has restored is a hope. Files: `tests/restore.test.ts` ### Done when - [ ] A crash mid-write leaves the previous vault intact - [ ] The new file is verified before replacing - [ ] Backups are automatic with a stated retention - [ ] Restore is exercised in the suite ### Watch out - Writing over the vault in place means a crash, a full disk or a battery dying mid-write leaves a truncated file and every password gone, and this loses more vaults than attackers do. Write elsewhere, verify it opens, then replace, and keep the previous generation. ## M3 · Entries, history and the awkward fields A vault holds real credentials including the ones that do not fit a template. ### Steps 1. Keep field history A changed password sometimes needs to be changed back. Files: `src/history.ts` 2. Search in memory, never index to disk A plaintext search index defeats the encryption entirely. Files: `src/search.ts`, `tests/no-plaintext-index.test.ts` 3. Verify code generation against test vectors Time-based codes are easy to get subtly wrong. Files: `src/otp.ts`, `src/otp.test.ts` ### Done when - [ ] Password history is retained and viewable - [ ] No plaintext index is written to disk - [ ] Attachments are encrypted and size bounded - [ ] One-time codes match known test vectors ### Watch out - Overwriting a password without keeping the old one means a site that rejects the change, or a device still using the old credential, leaves the user locked out with no way back. Keep history, and make it easy to see the previous value. ## M4 · Sync that never picks a winner Two devices editing the vault do not lose an entry. ### Steps 1. Detect divergence by version Not by timestamp. Clocks disagree and the loser is a password. Files: `src/sync.ts` 2. Keep both sides of a conflict Merging at entry level with the user deciding. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Never overwrite an unseen version The remote may hold the only copy of something. Files: `src/sync.ts` ### Done when - [ ] Offline edits to different entries merge without loss - [ ] Same-entry conflicts keep both values - [ ] An unseen remote version is never overwritten - [ ] Sync failure leaves the local vault usable ### Watch out - Resolving a sync conflict by taking the most recently written file discards every change made on the other device, and since a vault is edited rarely the loss is discovered months later when a password does not work. Detect divergence, keep both, and merge at entry level with the user choosing. ## M5 · Getting credentials out safely (production only) Filling and exporting work without leaving secrets somewhere unexpected. ### Steps 1. Match on the registrable domain Substring matching is how a phishing page gets your bank password. Files: `src/matching.ts`, `tests/phishing.test.ts` 2. Clear the clipboard on a timer And verify it cleared rather than assuming. Files: `src/clipboard.ts` 3. Lock on idle and on sleep A closed laptop should not have an unlocked vault in memory. Files: `src/lock.ts` ### Done when - [ ] A lookalike domain is refused - [ ] The clipboard clears and the clearing is verified - [ ] Plaintext export warns unsuppressibly - [ ] The vault locks on idle and sleep ### Watch out - Matching a saved credential to a page by checking whether the stored address appears anywhere in the current one will offer a bank password to a phishing page whose address contains the bank's name, which is exactly how the attack works. Match on the registrable domain, and never on a substring.
# Operations · Enpass ## Backup This is the one build where backup is the primary feature rather than an operational concern. Automatic generations, offsite copies, and a restore exercised in the suite, because losing a vault loses everything the user stopped remembering. ## Restore Restore and confirm the vault opens with the master password and that its recorded derivation parameters were used rather than the current defaults. A restore that cannot open the file is the failure this build exists to prevent. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: no plaintext index, no secret in a log, an old-parameter vault still opening, and a crash mid-write leaving the previous vault intact. ## Incident checklist An unopenable vault is the incident, and it is far more likely to come from a truncated write or a format change than from an attacker. The generations and the recorded parameters are what make recovery possible rather than theoretical. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An old-parameter vault is confirmed to open after defaults change - [ ] A crash mid-write is confirmed to leave the previous vault intact - [ ] No plaintext index is confirmed written to disk - [ ] A same-entry sync conflict is confirmed to keep both values - [ ] A lookalike domain is confirmed refused ## Launch constraint Do not market omitted Enpass capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The encrypted vault. Everything you have, so back it up. VAULT_PATH=~/.local/share/vault/vault.enc # Required. Copied from current guidance and written into each vault it produces. KDF_PARAMS=argon2id:m=47104,t=1,p=1 # Required. Automatic generations. The cheapest insurance in this category. BACKUP_DIR=~/.local/share/vault/backups # Required. How many previous vaults to retain. Encrypted, so keeping several is safe. BACKUP_KEEP=10 # Required. After copying a credential. Verified to have cleared. CLIPBOARD_CLEAR_SECONDS=30 # Required. Also locks on sleep. An unlocked vault in memory is the exposure. LOCK_IDLE_MINUTES=5
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Enpass because security products are paid for because expert review, infrastructure, and accountability matter more than recreating screens. The recurring cost buys cryptography, secure updates, key recovery, threat intelligence, relay capacity, abuse response, audits, and incident handling, not just the visible interface.
xmature apps, browser extensions, platform biometrics, and ongoing security maintenance
xindependent security audits
xglobal relay infrastructure
xbreach monitoring data
xaccount recovery and support
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Enpass pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Unlimited desktop use on Windows, macOS and Linux; mobile vault limited to 25 items |
| individual | — | $1.99/user | 1 person; unlimited vault items and devices across desktop and mobile |
| family | — | $2.99/workspace | Up to 6 family members; unlimited vault items and devices |
free tierUnlimited desktop vault items; mobile sync/use is capped at 25 items
billingannual only for current paid subscriptions; first-year promotional pricing
hidden costsThe Family plan rises from $35.99 to $47.99 at renewal; Enpass does not include cloud storage, so the user's Dropbox/Google Drive/OneDrive/iCloud or WebDAV storage may carry a separate cost
verified 2026-08-14 · source ↗
Vibecode Enpass
Kinda. The core of Enpass is buildable in a weekend with the prompt on this page, but there are real gaps: mature apps, browser extensions, platform biometrics, and ongoing security maintenance, independent security audits. Read the honest list above before committing.
How much does Enpass cost?
Enpass costs about $1.99/month (Individual, checked 2026-07-31), which is $23.88 per year.
What do I lose by replacing Enpass?
Honestly: mature apps, browser extensions, platform biometrics, and ongoing security maintenance; independent security audits; global relay infrastructure; breach monitoring data; account recovery and support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Enpass?
Yes: KeePassXC (A local encrypted vault with browser autofill; put the database in your own sync folder and own the conflicts.) The prompt is for when you want it exactly your way.