Build Enpass
KINDAreplaces $1.99/mosaves $23.88/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the local application.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
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 - have readyfree
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. open ↗
Verify
A written note records the parameters and the guidance version followed - have readypennies 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 - have readyfree
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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
VAULT_PATH | required | ~/.local/share/vault/vault.enc | The encrypted vault. Everything you have, so back it up. |
KDF_PARAMS | required | argon2id:m=47104,t=1,p=1 | Copied from current guidance and written into each vault it produces. |
BACKUP_DIR | required | ~/.local/share/vault/backups | Automatic generations. The cheapest insurance in this category. |
BACKUP_KEEP | required | 10 | How many previous vaults to retain. Encrypted, so keeping several is safe. |
CLIPBOARD_CLEAR_SECONDS | required | 30 | After copying a credential. Verified to have cleared. |
LOCK_IDLE_MINUTES | required | 5 | Also locks on sleep. An unlocked vault in memory is the exposure. |
The build, in order
A vault you can still open next year
Encryption uses vetted primitives and the file records how to decrypt it.
Copied from the guidance, stored in the file, with the version noted.
Files
src/crypto.tsdocs/crypto-parameters.mdTampering should fail loudly rather than decrypt to nonsense.
Files
src/crypto.tsNever to disk, never to a log, and zeroed when locked.
Files
src/session.tstests/no-secret-leak.test.ts
done when · tick each as it passeswatch 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.
Writes that cannot half happen
A crash mid-save never leaves an unreadable vault.
Verify by opening the new file before it becomes the vault.
Files
src/storage.tsThe cheapest insurance against every failure in this category.
Files
src/backups.tsA backup nobody has restored is a hope.
Files
tests/restore.test.ts
done when · tick each as it passeswatch 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.
Entries, history and the awkward fields
A vault holds real credentials including the ones that do not fit a template.
A changed password sometimes needs to be changed back.
Files
src/history.tsA plaintext search index defeats the encryption entirely.
Files
src/search.tstests/no-plaintext-index.test.tsTime-based codes are easy to get subtly wrong.
Files
src/otp.tssrc/otp.test.ts
done when · tick each as it passeswatch 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.
Sync that never picks a winner
Two devices editing the vault do not lose an entry.
Not by timestamp. Clocks disagree and the loser is a password.
Files
src/sync.tsMerging at entry level with the user deciding.
Files
src/conflicts.tstests/conflict.test.tsThe remote may hold the only copy of something.
Files
src/sync.ts
done when · tick each as it passeswatch 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.
Getting credentials out safelyproduct builder
Filling and exporting work without leaving secrets somewhere unexpected.
Substring matching is how a phishing page gets your bank password.
Files
src/matching.tstests/phishing.test.tsAnd verify it cleared rather than assuming.
Files
src/clipboard.tsA closed laptop should not have an unlocked vault in memory.
Files
src/lock.ts
done when · tick each as it passeswatch 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.
That is the whole plan for Enpass. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more security.