Security Whitepaper
Version 1.0 | Last updated: November 5, 2026
This document explains how KeyMe Pass encrypts your data: which algorithms we use, with which parameters, where keys are created and where they are kept, what we can see, and what we have not done yet.
Everything here is already implemented in the code and can be checked claim by claim. When the implementation changes, this document changes with it.
1. Design Principles
1.1 Zero Knowledge
A vault is encrypted on the device before it ever leaves it. What the backup server holds is ciphertext, and no code path on the server can decrypt user data.
This is not a promise but an architectural consequence: the key needed for decryption is derived on the device from the user's master password, and the master password is never uploaded.
1.2 The User Decides Where Data Goes
A vault can:
- stay on the device only, fully offline
- sync to storage hosted by KeyMe Pass
- sync to the user's own WebDAV server
Client-side encryption behaves identically in all three modes (see §4.3).
1.3 Why These Details Are Public
We follow Kerckhoffs's principle: the security of a system should rest on the key alone, never on the secrecy of the algorithm.
If publishing the encryption design made the system attackable, the system was never secure to begin with — it was merely obscure. This document therefore goes down to the parameter level.
What this document deliberately excludes: server deployment topology, storage organisation, and the exact on-disk paths of key material. Those are attack-surface reconnaissance, unrelated to cryptographic design; publishing them adds nothing a reviewer can verify.
2. Key Model
2.1 Overview
Master password
│
│ Argon2id (m=64MiB, t=3, p=4)
▼
vaultKEK ──┐
│ AES-256-GCM wrap
Recovery key│
│ HKDF ├──────────► DEK ──────► encrypts vault data
▼ │ (32 bytes)
recoveryKEK┤
│
PIN │
│ Argon2id
▼ │
pinKEK ────┤
│
Biometrics │
│ (retrieve random key)
▼ │
bioKEK ────┘
The central fact: all four slots wrap the same DEK.
The DEK (Data Encryption Key) is the key that actually encrypts vault contents. The four paths — master password, recovery key, PIN, biometrics — each derive a different KEK (Key Encryption Key), but every one of them wraps the same DEK.
This yields an important property: changing the master password, resetting with the recovery key, and rotating the recovery key all replace only the wrapping of the DEK; the DEK itself never changes. None of these operations requires re-encrypting the vault.
One device = one local dataset = one DEK. The DEK changes only when switching accounts (data consolidation) or on a reset that clears the vault.
2.2 Master Password → vaultKEK
Algorithm: Argon2id (RFC 9106), with these parameters:
| Parameter | Value |
|---|---|
| Memory (m) | 65536 KiB (64 MiB) |
| Iterations (t) | 3 |
| Parallelism (p) | 4 |
| Output length | 32 bytes (256 bits) |
| Version | 0x13 (19) |
| secret (K) / associatedData (AD) | empty |
Salt: 16 bytes from a CSPRNG (PointyCastle FortunaRandom, seeded from the operating system's secure random source). Unique per account.
Argon2id was chosen over PBKDF2 because it resists both GPU parallelism and side-channel attacks. The 64 MiB memory cost raises the hardware price of large-scale parallel cracking substantially.
Cross-platform consistency: six platforms run three implementations (native C library / HarmonyOS FFI / pure Dart), all referencing one shared set of parameter constants. When key material is written, a byte-level cross-check runs: the pure-Dart implementation recomputes the result and compares it to the native one in constant time. On any mismatch the Dart result is persisted and the platform's native implementation is permanently disabled.
Parameters cannot be tampered with remotely: although the key-material file records KDF parameters, the client reads only the salt from it and always uses local constants for the parameters. Tampering with server-side data cannot weaken any user's KDF.
2.3 The DEK
- Generation: 32 bytes from a CSPRNG (as above)
- Wrapping: AES-256-GCM, 12-byte random IV, 128-bit authentication tag
- The plaintext DEK exists only in memory and is zeroed in place on lock
DEK identity sentinel: when the DEK is first generated, a fixed plaintext is encrypted with it and the ciphertext stored. After unlocking, this sentinel verifies that the DEK matches the local dataset, preventing an abnormal state from overwriting data with the wrong key.
2.4 The Four Slots Derive Their KEKs Differently
| Slot | KEK source | Notes |
|---|---|---|
| Master password | Argon2id(master password, account salt) | See §2.2 |
| Recovery key | HKDF-SHA256(RK, salt, info=keyme:recovery_v1) | No Argon2 — see below |
| PIN | Argon2id(PIN, separate salt) | Same parameters as §2.2 |
| Biometrics | 32 random bytes held in the platform keystore | No cryptographic relationship to the fingerprint or face; on most platforms retrieval requires system biometric verification, see §7 |
Why the recovery key skips Argon2id: the recovery key is a system-generated 125-bit random value (§6) that already carries sufficient entropy, so there is no "weak passphrase" problem. A slow hash exists to throttle brute force against low-entropy secrets; against a high-entropy random value it buys nothing.
The master password and PIN do use Argon2id, because users choose them and their entropy cannot be controlled.
2.5 Slot Isolation
This section describes the current version. In earlier versions the PIN and biometric slots did not have the isolation described here; existing devices upgrade automatically (see "Existing devices and the transition period" below).
All four slots use additional authenticated data (AAD) that differs per slot: DEK_WRAP:{master|pin|bio|recovery}:v2. Which AAD is used when unwrapping is determined by *which slot is being read*, never taken from the data itself. Moving ciphertext from one slot into another makes GCM authentication fail outright.
Existing devices and the transition period (stated plainly, so this is not read as "every device is already isolated"):
- On V2 accounts, PIN and biometric slots created by earlier versions use the unified AAD (
DEK_WRAP); they upgrade in place the next time the user unlocks successfully with a PIN or biometrics, with no action required;- On accounts still using the older key architecture (V1), the PIN and biometric slots keep the unified AAD and are rebuilt when the account is upgraded to V2;
- The read path therefore still accepts the unified AAD during the transition. An attacker able to write to local storage could use this to revert a slot to the unified-AAD format (unwrapping still requires that slot's own key). This fallback is removed together with V1.
Exploiting that fallback requires local file-write access — and under that precondition more direct attack paths already exist. Moreover, the four slots derive their KEKs from different sources (§2.4), so even with identical AAD the KEK of slot A cannot open slot B. AAD isolation is one layer of defence in depth, not the only barrier.
3. Local Data Encryption
⚠️ The three paths in this section use different algorithms and must not be summarised as one.
3.1 Structured Data (passwords, identity documents, one-time codes, notes, SSH keys, payment cards, passkeys)
- Storage: encrypted database (Hive encrypted box)
- Algorithm: AES-256-CBC with PKCS7 padding, 16-byte random IV (regenerated on every write)
- Key: the in-memory DEK, used directly with no secondary derivation
- If the DEK is unavailable the encrypted store refuses to open; it never falls back to plaintext
On integrity: CBC provides confidentiality but not authentication. Tampered ciphertext does not raise an error on decryption; it simply yields meaningless data. Data integrity relies on operating-system file protection and the application sandbox.
This is the embedded database's built-in implementation. Under the precondition "the attacker can write to the application's private directory", that attacker usually has more direct means available, so we accept this boundary — but we do not describe it as authenticated encryption.
Data that is not encrypted: basic account configuration, application settings, subscription status, and sync tombstone records. These must be readable before unlocking and contain no vault content.
3.2 Attachments and Photos — Not Encrypted Locally
Attachment files stored on the device (document photos, general attachments) are written to disk in plaintext.
Protection relies on the operating-system application sandbox and full-disk encryption, not on application-layer encryption.
Why we disclose this: it is the actual state of the implementation. The design stems from content-addressed storage (one copy per identical file), where a file's address is determined by the hash of its content. Moving to encrypted storage requires redesigning deduplication at the same time; this is tracked as a separate work item.
Where it sits in the threat model: an attacker who can read the application's private directory can, under the same precondition, usually also read the DEK from memory, so encrypting attachments adds limited value in that scenario. We do not, however, use that as a reason to call the current state "encrypted".
Attachments are encrypted when uploaded to the cloud — see §4.2.
3.3 Import / Export / Upgrade Snapshots
AES-256-GCM with a 12-byte random IV and the file name or file path as AAD, written as IV(12) || auth tag(16) || ciphertext.
For encrypted exports the key is derived with Argon2id from the export password the user sets, with a freshly generated salt each time.
4. Sync and the Server
4.1 The Encryption Boundary: Whole-File Encryption
Objects in the sync directory:
| Object | Encrypted |
|---|---|
| Per-entity sync files (passwords / one-time codes / identity documents / payment cards / passkeys / notes / SSH keys / attachment index / categories) | ✅ AES-256-GCM, whole file |
| Compatibility mirror files (read by older clients) | ✅ AES-256-GCM, whole file |
| Attachment files | ✅ AES-256-GCM, per file |
| Root metadata file | ❌ plaintext, see §4.4 |
Encryption granularity is the whole sync file — all entries inside one password sync file share a single encryption operation; an individual entry is not a separate ciphertext unit.
Ciphertext envelope (JSON, fields Base64-encoded):
{ "version": 1, "dek_version": 2, "cipher": "...", "nonce": "<12-byte IV>", "tag": "<16-byte auth tag>" }
4.2 Attachment Sync
AES-256-GCM, 12-byte random IV, laid out as IV(12) || auth tag(16) || ciphertext. The key is the same DEK.
4.3 Hosted Sync and Self-Hosted WebDAV Behave Identically
Both modes use exactly the same encryption path. The client has already settled on the local DEK *before* it reads the sync configuration, so file format, algorithm, and key are identical in both modes.
The modes differ only in storage location and authentication.
📌 One boundary worth stating explicitly: when using self-hosted WebDAV, the ciphertext lives on a third-party server, but the key material (the DEK wrapped by the master password and by the recovery key) remains on KeyMe Pass servers. The two are separated; neither party alone can decrypt.
4.4 What the Server and Third Parties Can See
We consider an honest disclosure of the metadata boundary more valuable than a sweeping claim that "the server sees nothing".
✅ Visible (the root metadata file is plaintext)
| Content | Notes |
|---|---|
| Which entity categories have sync enabled | The file name is the category |
| The byte size of each category's ciphertext | Loosely correlated with entry count |
| Sync version number and last-updated time | |
| The client identifier of the submitting device | A random UUID, not a hardware identifier |
| The list of attachment identifiers | See the risk note below |
❌ Not visible (all of this sits inside the ciphertext)
Entry counts, field names, titles, URLs, usernames, passwords, notes, tag names, folder names.
🚨 Attachment identifiers are probeable (a real risk we must state)
An attachment's remote object name is built from the first 16 characters of the SHA-256 of its plaintext content.
This means: a party already holding a particular file can compute its hash and confirm whether a given user stores that file — though they cannot read its contents.
This is an inherent consequence of content-addressed storage. The planned fix is to use an HMAC keyed with the DEK as the address; it is tracked as a work item.
✅ File fingerprints do not leak content: the fingerprint field in the metadata is an HMAC-SHA256 keyed with the DEK, not a plaintext hash, so it cannot be used for known-plaintext comparison.
4.5 What the Server Stores
| Item | Content | Can it decrypt user data? |
|---|---|---|
| Wrapped DEK (two copies: master password / recovery key) | Ciphertext | ❌ Requires the master password or recovery key |
| KDF salt | Plaintext | ❌ A salt is not a secret |
| bcrypt hash of the login token | Hash | ❌ |
| bcrypt hash of the recovery-key verifier | Hash | ❌ |
| Account email, device information, storage usage | Plaintext | Contains no vault content |
A full search of the server codebase confirms: no code path decrypts user sync data.
5. Authentication
5.1 The Master Password Is Never Uploaded in Any Form
What is uploaded is a derived value:
loginToken = HKDF-SHA256(ikm = NFKC-normalised master password, salt = empty, info = "keyme:login_v1", L = 32)
The server stores bcrypt(loginToken, cost=10), never the loginToken itself.
loginToken and vaultKEK are two independent derivations — even in possession of the loginToken, the server cannot obtain the vaultKEK, which comes from Argon2id with a separate salt.
5.2 The Limits of This Design (stated plainly)
HKDF is a one-way function; the master password cannot be recovered from the loginToken.
⚠️ However, the loginToken derivation uses no per-user salt, and the info string is identical for every user. It is therefore a deterministic function of the master password: the same master password always yields the same loginToken.
Consequences:
- No rainbow-table attack at the database level ✅ — the server stores a salted bcrypt hash
- But if a plaintext loginToken leaks anywhere, the cost of one offline dictionary guess is a single HKDF-SHA256 — far cheaper than Argon2id
The accurate statement is: the loginToken cannot be reversed, but weak passphrases remain subject to dictionary attack.
The vault itself is unaffected, since the vaultKEK goes through Argon2id.
The planned improvement (introducing a per-user salt) is tracked as a work item; it requires version negotiation and a migration period.
5.3 Anti-Enumeration
Login failures do not distinguish "account does not exist" from "wrong credentials". Every branch of the recovery flow performs the same number of bcrypt comparisons, eliminating timing differences.
6. The Recovery Key
6.1 Generation
- Entropy: 125 bits (25 five-bit indices from the operating system's secure random source)
- Alphabet: Crockford Base32 (excluding the easily confused I / L / O / U)
- The 26th character is a check character that reduces transcription errors (not a cryptographic check)
- Displayed in groups of four characters
6.2 What It Does
The recovery key derives two independent values:
- The recovery KEK (opens the recovery slot locally):
HKDF-SHA256(RK, salt, "keyme:recovery_v1") - The server-side verifier:
HKDF-SHA256(RK, empty salt, "keyme:rk_verifier_v1"), of which the server stores only a bcrypt hash
6.3 Minimal Exposure in the Recovery Flow
After three-factor verification (email + email code + recovery-key verifier), the server returns only the DEK copy wrapped by the recovery key. It never returns the copy wrapped by the master password, and never returns the master password's KDF salt.
The reason: the master-password copy plus its salt is exactly the material needed to brute-force the master password offline.
Two-phase commit: a reset or rotation first writes a staging record while the live data stays untouched; it takes effect in a single transaction only after the user confirms they have written down the new recovery key. The old password and old recovery key keep working throughout the staging period.
6.4 An Irreversible Consequence
We cannot reset a user's master password. If it is forgotten, the recovery key is the only way back in. If both are lost, the vault data is permanently undecryptable — and no support channel can work around this.
That is the price of a zero-knowledge architecture: a password we could reset for a user is a password someone else might find a way to reset too.
7. Platform Key Custody and Biometrics
§7.1–§7.3 describe the current version. In earlier versions, retrieving the bioKEK did not require system verification; after upgrading, biometric unlock switches to the model described here the next time it is enabled. Platforms that still lack cryptographic binding (Android weak-class, Windows, Linux) are listed honestly in §7.2 and are not omitted because "most platforms do it".
7.1 The Model Behind Biometric Unlock
Biometrics take no part in key derivation. The bioKEK is a 32-byte random key with no cryptographic relationship to a fingerprint or face; its job is to wrap the DEK. What differs between platforms is who guards the bioKEK:
System biometric check ──► keystore releases bioKEK ──► unwrap ──► DEK
(authentication and retrieval are one system call; the keystore performs the check)
On iOS / macOS, Android (strong-class devices), and HarmonyOS, the bioKEK is stored in the system keystore with the requirement "retrievable only after biometric verification" declared at creation time. The system enforces this and application code cannot bypass it: without passing that specific system authentication, the bioKEK cannot be obtained.
7.2 Two Layers of Protection, and Which Platforms Reach Which
| iOS / macOS | Android strong | Android weak | HarmonyOS | Windows | Linux | |
|---|---|---|---|---|---|---|
| bioKEK held in the system keystore, unreadable by other apps | ✅ | ✅ | ✅ | ✅ | Current user context | Depends on distribution |
| System enforces biometric verification on retrieval | ✅ | ✅ | ❌ app-level check | ✅ | ❌ app-level check | No biometrics |
| Invalidated when enrolled biometrics change | ✅ adding/removing a fingerprint or re-enrolling a face invalidates the whole key | ✅ invalidated when a new biometric is enrolled (removal alone does not) | — | ⚠️ per credential type, invalidated on enrolment only (see below) | — | — |
| Screen-lock passcode can substitute for biometrics | ❌ | ❌ | ❌ | ❌ | ⚠️ yes (Windows Hello PIN) | — |
Why Android is split by class. When the system is asked to bind a key to authentication, it accepts only strong-class (Class 3) biometrics; face recognition on many devices is weak-class (Class 2) and cannot be used for such keys. We decide the class from the device's capability at the moment biometric unlock is enabled: a device with strong-class biometrics gets system-enforced verification; a weak-class-only device stores the key in the same system keystore but without authentication binding, and the biometric check before retrieval is performed by the application (the ❌ in the "system enforces" row of this table). This carries a known cost: on strong-class devices the system prompt accepts only strong-class biometrics, so a user who has enrolled both a fingerprint and a weak-class face can unlock only with the fingerprint.
HarmonyOS invalidation semantics. The strictest setting the platform offers (INVALID_NEW_BIO_ENROLL) works per credential type: after a new biometric of a given type is enrolled, the key can no longer be used via that type, while the other type is unaffected (enrolling a new fingerprint disables fingerprint but leaves face working, for example). Removing a biometric does not invalidate the key; clearing the screen-lock passcode invalidates it entirely. This is looser than the iOS rule of "any change invalidates everything", and it is the strictest option available through that platform's API.
The key is bound to the types already enrolled when biometric unlock was switched on (the system does not allow binding a type that is not yet enrolled): if only a fingerprint was enrolled at the time, enrolling a face later requires switching biometric unlock off and on again before the face can be used. At unlock time only types that are both bound and still enrolled are requested (after deleting a face, for example, it automatically falls back to the fingerprint). If no biometric remains enrolled, biometric unlock is switched off.
After invalidation. When the system invalidates the key, the application recognises the invalidated state, turns biometric unlock off, and asks the user to enable it again. It does not silently rebuild the key — rebuilding would bypass the very signal that the enrolled biometric set has changed. The master password is unaffected, and re-enabling takes one biometric verification. Upgrading from an older version to the V2 key architecture likewise turns biometric unlock off and requires re-enabling, because the upgrade must rewrap with the new DEK and the bioKEK can no longer be read without authentication.
Extension processes. Autofill and passkeys run in system extension processes. They read the same protected bioKEK (on Apple platforms through a shared keychain access group; on Android through native code inside the same application process) and likewise require the current system authentication. No plaintext copy exists for extensions to use.
Why Windows and Linux do not reach this bar. Windows DPAPI decrypts per "current user", and Windows Hello is only an interface gate before retrieval (which also accepts the system PIN). Linux offers no usable biometric interface. Achieving cryptographic binding on Windows would require moving to NCrypt / Windows Hello key credentials; that is future work.
7.3 Per-Platform Detail
| Platform | Where the bioKEK lives | Protection | Invalidation triggers |
|---|---|---|---|
| iOS / macOS | Keychain (shared access group; data protection keychain on macOS) | .biometryCurrentSet + WhenPasscodeSetThisDeviceOnly: this device only, requires a screen-lock passcode, excluded from backups | Adding/removing a fingerprint or re-enrolling a face; disabling the screen-lock passcode |
| Android strong | An Android Keystore key (AES-256-GCM) encrypts it into application-private storage | Strong-class biometrics required on every use (screen-lock passcode not accepted), verified by the system through CryptoObject | Enrolling a new biometric; disabling the screen lock |
| Android weak | As above, but the key is not bound to authentication | Application-level biometric check | — |
| HarmonyOS | A HUKS key (AES-256-GCM, nonce generated by HUKS) encrypts it into application Preferences | Decryption requires authentication with a face or fingerprint enrolled at setup time (screen-lock PIN excluded), with HUKS verifying the authentication token | Enrolling a new biometric of that type; clearing the screen-lock passcode |
| Windows | A DPAPI-encrypted file | Current user context; any process running as that user can decrypt it | — |
| Linux | Secret Service (libsecret) | Depends on the distribution; no biometrics | — |
Encryption of structured sync files is unaffected by these differences — that layer is AES-256-GCM with the same DEK on every platform. Platform differences affect only how unlock material is held locally.
7.4 PIN Unlock
- The PIN is six digits (roughly 20 bits of entropy)
- Argon2id (parameters as in §2.2) derives the KEK that opens the PIN slot
- Verification is "does it unwrap"; there is no separate PIN verifier stored
- No server involvement
Failure lockout ladder (inside the main application): 30 seconds → 1 minute → 2 minutes → 5 minutes → 15 minutes → 1 hour → 3 hours → 8 hours → 24 hours, nine levels in total.
Design details:
- Expiry of a cooldown does not step the level back down; only a successful unlock resets it — otherwise waiting would be a way around the ladder
- A further failure during a cooldown stacks the new cooldown onto the original expiry
- Each level up reduces the number of permitted attempts (floor of two)
- State is persisted and survives a process restart
⚠️ PIN unlock inside extension processes does not go through this lockout mechanism. On that path the only cost of brute force is the Argon2id computation itself (roughly 64 MiB of memory and three iterations per attempt).
7.5 Key Caching in Extension Processes
Autofill and passkeys run in system extension processes. They never hold the master password and obtain the DEK through one of three paths: a cache hit, biometrics, or the PIN.
| Platform | Cache medium | Lifetime |
|---|---|---|
| Android | Memory only, never written to disk | 15 minutes |
| iOS / macOS | Stored in plaintext in the shared Keychain | 15 minutes |
On iOS / macOS the cached DEK is protected by the Keychain's own attributes (readable only while the device is unlocked, excluded from backups) together with App Group access control.
A one-way constraint: the channel between the main application and extension processes permits writes and clears only — the DEK can never be read back. Otherwise, once a user unlocked for autofill inside another application, returning to the main application would find it silently unlocked, bypassing the main application's lock semantics.
8. Passkeys
8.1 Key Generation
Algorithm: ES256 (ECDSA P-256 / secp256r1 with SHA-256), signing with RFC 6979 deterministic k and low-S normalisation.
Private keys are software-generated and exportable; they are not held by a hardware security module.
This follows necessarily from supporting cross-device sync — hardware-bound non-exportable keys and "use the same passkey on several devices" are mutually exclusive.
8.2 Private Key Custody
A private key is never written to disk in plaintext:
- Encryption: AES-256-GCM, 12-byte IV, 128-bit authentication tag
- The credential ID is the AAD — which makes a private key's ciphertext untransplantable to another credential
- It uses the same DEK as ordinary entries; only the AAD differs
- The plaintext private key bytes are zeroed immediately after generation
8.3 Sync
Under the current architecture, passkeys sync along with the vault and the server sees only ciphertext.
8.4 User Verification Status Is Reported Honestly
When a relying party requires userVerification: "required", biometric or PIN verification is enforced and the cached DEK is not used.
When the relying party does not require it, a cached DEK may be reused within its lifetime — and in that case the user-verification flag in the authenticator data is honestly reported as "not verified".
Registration always enforces verification and never uses the cache.
An earlier version always set that flag to "verified", which amounted to misreporting the verification status to relying parties. It has been fixed. We record it here because a security document that lists only strengths is worthless.
9. Threat Model
**We consider stating what we do *not* defend against as important as stating what we do.** A security document that lists only capabilities cannot help a user judge anything.
9.1 What We Defend Against
| Threat | How |
|---|---|
| Server compromise / insider access | The server holds only ciphertext and wrapped keys, with no decryption path |
| Interception in transit | Content is encrypted on the device before transmission — a protection layer independent of TLS |
| A third-party WebDAV provider snooping | As above; such a provider sees only ciphertext and the metadata in §4.4 |
| Device loss (locked state) | The vault is encrypted at rest and the DEK is never on disk in plaintext |
| Offline cracking after a database leak | Argon2id (64 MiB / 3 iterations) raises the cost of each attempt substantially |
| Cross-slot ciphertext substitution | Per-slot AAD isolation (§2.5) |
| Remote tampering with KDF parameters | The client reads only the salt remotely; parameters are always local constants |
| Account enumeration | Login does not distinguish nonexistent accounts from bad credentials; recovery compares in constant time |
| Abusing recovery to crack the master password | After verification only the recovery-key copy is returned — never the master-password copy or its salt |
9.2 What We Do Not Defend Against
The following are beyond this product's protection. We list them rather than avoid them.
| Scenario | Notes |
|---|---|
| A rooted or jailbroken device | The attacker can read application-private directories and process memory. Under this precondition, local plaintext attachments and the in-memory DEK while unlocked are both obtainable. As for the bioKEK: on iOS / macOS, Android strong-class, and HarmonyOS, retrieval requires system biometric verification and it cannot be read out directly; on Android weak-class, Windows, and Linux it can be read (§7.2) |
| A malicious keyboard, screen recording, or keylogger | The master password can be captured as it is typed |
| Shoulder surfing or a camera | |
| Physical coercion | We have no duress-password mechanism |
| A weak master password | Argon2id raises the cost but cannot compensate for low entropy |
| Operating-system or hardware vulnerabilities | |
| A user disclosing their own recovery key | |
| Reading local attachment files | Attachments are stored locally in plaintext (§3.2) and rely on the system sandbox |
| Probing file possession via attachment hashes | See §4.4: one can confirm whether a specific file is held, but not read it |
| Brute-forcing the PIN inside an extension process | That path has no lockout ladder (§7.4); the only cost is the KDF computation |
| A malicious system-level autofill service | If the user sets one as the system default autofill provider |
| Targeted tampering with the local database | CBC provides no authentication (§3.1); data can be corrupted, though not easily forged in a targeted way |
| Supply-chain attacks | We reduce risk by pinning dependency versions, but we do not claim full protection |
9.3 On Those Boundaries
Most of the items above share one precondition: the attacker already has privileged access to the device. Under that precondition almost no local password manager can offer effective protection, because the ability to decrypt must exist in memory at some point.
We list them not because they are weaknesses unique to this product, but because users are entitled to know where their protection ends.
10. Known Limitations
The following are things we have not done. In a review context, concealment costs far more than disclosure.
10.1 The Product Is Closed-Source
The source code is not public. We do not present this as a strength — it genuinely makes external review harder, and it means the claims in this document cannot be confirmed by a third party reading the code directly.
10.2 No Independent Third-Party Security Audit Yet
There is currently no audit report issued and published by an independent firm. One is planned, but until it is complete we will not claim to have had one.
10.3 Outside the Scope of This Document
- The encryption implementation of the browser extension (not covered by this revision's review, so no claims are made about it)
- Server-side encryption configuration at the object-storage layer (our security guarantees do not depend on it)
11. Reporting Security Issues
To report a vulnerability, email support@keymepass.com with "Security" in the subject line and we will prioritise it.
Please do not disclose details publicly before a fix ships — that protects the people currently using the product.
We commit to:
- acknowledging reports promptly
- crediting the reporter in the release notes after a fix (if they wish to be named)
- taking no legal action against good-faith security research
Appendix: Algorithms and Parameters
| Purpose | Algorithm | Parameters |
|---|---|---|
| Master password / PIN → KEK | Argon2id (RFC 9106) | m=64 MiB, t=3, p=4, 32 B output, v=0x13 |
| Recovery key → KEK | HKDF-SHA256 | info=keyme:recovery_v1, 32 B output |
| Login token derivation | HKDF-SHA256 | info=keyme:login_v1, no salt, 32 B output |
| DEK wrapping | AES-256-GCM | 12 B IV, 128-bit tag |
| Sync file encryption | AES-256-GCM | 12 B IV, 128-bit tag |
| Attachment cloud encryption | AES-256-GCM | 12 B IV, 128-bit tag |
| Import / export encryption | AES-256-GCM | 12 B IV, 128-bit tag, AAD = file name |
| Local database | AES-256-CBC + PKCS7 | 16 B IV, no authentication tag |
| Passkey signing | ECDSA P-256 + SHA-256 | RFC 6979 deterministic k, low-S |
| Passkey private key encryption | AES-256-GCM | 12 B IV, 128-bit tag, AAD = credential ID |
| Server-side credential storage | bcrypt | cost=10 |
| Random number generation | CSPRNG | PointyCastle Fortuna, seeded from the system secure random source |
*This document is updated as the implementation evolves. Any change to a parameter or design must be reflected here in the same release.*