Skip to content

Security: YassinLokhat/Upsilon.Apps.Passkey

SECURITY.md

Security Policy

Upsilon.Apps.Passkey is a local-only password manager. There is no server, no account, and no synchronization: every secret lives inside a single encrypted .pku file on the user's device. Security is therefore the core feature of this project, and this document describes exactly how the current implementation protects data and how to report a problem.

Supported Versions

Each component is versioned independently and may evolve at its own pace. Security fixes are applied to the latest released version of each component only, so please always upgrade to the most recent release before reporting an issue.

At the time of writing, `Upsilon.Apps.Passkey.GUI.WPF`, `Upsilon.Apps.Passkey.Core`, `Upsilon.Apps.Passkey.Utils` and `Upsilon.Apps.Passkey.Interfaces` are on **2.0.x**. Each assembly is versioned independently and may diverge.

Versions and dependency ranges are maintained in versions.json.

Component (assembly) Supported version Supported
Upsilon.Apps.Passkey.GUI.WPF (app) 2.0.x ✅
Upsilon.Apps.Passkey.Core 2.0.x ✅
Upsilon.Apps.Passkey.Utils 2.0.x ✅
Upsilon.Apps.Passkey.Interfaces 2.0.x ✅

Any version older than the latest release of a given component is not supported.

Reporting a Vulnerability

Please do not open a public GitHub issue for security vulnerabilities.

Report vulnerabilities privately through either of the following channels:

Please include, when possible:

  • A description of the vulnerability and its impact.
  • Step-by-step reproduction instructions or a proof of concept.
  • The affected component and version (see the table above).
  • Any suggested remediation.

What to expect:

  • Acknowledgement: within 7 days.
  • Initial assessment: within 14 days, including whether the report is accepted, needs more information, or is declined (with a rationale).
  • Fix and disclosure: for accepted reports, a fix is prepared on a private branch and released as soon as reasonably possible. Coordinated disclosure is preferred; credit is given to reporters who wish to be acknowledged.

Threat Model

In scope

  • Confidentiality and integrity of the .pku database file at rest.
  • Resistance to offline brute-force against the master passkeys.
  • Friction against interactive (online) guessing during progressive login (no rollback of a wrong passkey; see "Progressive login without rollback").
  • Tamper detection of stored data.
  • Limiting exposure of secrets during an active session (auto-logout, clipboard cleaning).

Out of scope

  • A compromised host (malware, keylogger, memory scraper, or an attacker with code execution on the machine while the database is unlocked).
  • The security of the operating system, its clipboard, and its swap/hibernation files.
  • Physical access to an unlocked, logged-in session.
  • Plaintext files the user deliberately produces via Import/Export (see "Known Limitations").

Security Design

All cryptography is implemented in Utils/CryptographyCenter.cs on top of System.Security.Cryptography (the .NET BCL). The project has a strict zero-external-dependency policy for the Core, Utils, Interfaces, and WPF GUI projects: no third-party cryptographic package is used, which keeps the security-critical supply-chain attack surface minimal.

Master passkeys (multi-factor "onion")

  • A database is protected by an ordered set of passkeys (master passwords). All of them, in the correct order, are required to decrypt the data.
  • The onion always starts with an implicit first layer keyed by the username: GetHash(username) (fast SHA-512, Base64 with / replaced by -) is pushed onto the stack before any stretched passkey. Changing the username therefore changes the ciphertext layout; it is not a secret factor on its own, but it is part of the key material used at rest.
  • Each passkey is stretched with PBKDF2-HMAC-SHA-512, 1,000,000 iterations (64-byte output) before being used as key material (GetSlowHash). This far exceeds the OWASP baseline and makes offline guessing expensive. HMAC-SHA-512 is used deliberately: its 64-bit arithmetic is markedly less efficient on the GPUs and ASICs an attacker would use for parallel guessing than the 32-bit operations of SHA-256.
  • The PBKDF2 salt is a random 128-bit value generated once when the database is created and stored in the file's header entry. It is stable for the life of the file (required to reopen it) and unique per database, so two files never stretch the same passkey to the same key material — even with identical usernames and passkeys. A salt is not secret; storing it unencrypted is standard.
  • Crypto-agility: the stretching parameters (algorithm, iterations, output length, salt) are recorded in an unencrypted header entry of the .pku file. A database is always reopened — and rewritten — with the exact parameters stored in that header. There is no automatic upgrade to DefaultSlowHashParameters on save today; the sticky header is what will let a future release migrate work factor or algorithm without breaking existing files. The header is not secret: lowering iterations in an existing file's header does not weaken already encrypted data (the wrong work factor simply yields the wrong key). What it can do is offer the user a new vault written under a trivial work factor. To block that, Open and every GetSlowHash call enforce a KDF floor and ceiling via EnsureSufficientSlowHashParameters: a known algorithm, iterations at least 600,000 (PBKDF2-HMAC-SHA-256) or 210,000 (PBKDF2-HMAC-SHA-512) — the OWASP Password Storage Cheat Sheet baselines — and at most 5,000,000; output length between 32 and 128 bytes inclusive; and a Base64 salt of 16–64 bytes inclusive. Parameters outside that range raise InsufficientKdfParametersException and the file is refused (the ceiling blocks a forged header from demanding unbounded PBKDF2 work, allocation, or an oversized salt). New databases still use the stronger default of 1,000,000 PBKDF2-HMAC-SHA-512 iterations.

Symmetric encryption (data at rest)

  • The database and autosave payloads are protected with a layered ("onion") scheme: each passkey adds one authenticated AES-256-GCM layer.
  • For every layer, a fresh 32-byte key is derived with HKDF-SHA256 from the passkey and a random 16-byte salt; a random 12-byte nonce and a 16-byte authentication tag are used. Each layer is binary salt | nonce | tag | ciphertext; Base64 is applied once to the finished onion, so intermediate layers do not inflate the next by ~4/3.
  • AES-GCM is an AEAD cipher: any tampering with the ciphertext, nonce, or tag is detected and rejected on decryption.
  • A final layer keyed with a fixed, public value lets the code distinguish "corrupted or foreign data" (CorruptedSourceException) from "valid data, wrong passkey" (WrongPasswordException).

Asymmetric encryption (activity log)

  • Key pairs are RSA-4096, exported as PEM.
  • The audit/activity log uses a hybrid scheme: a random one-time AES key encrypts each record symmetrically, and that key is wrapped with RSA-OAEP-SHA256. This lets activity entries be written even when the full symmetric passkey set is not available (e.g. a failed login records an entry without anyone being logged in).

Activity-log integrity (tamper-evidence)

Because entries must be writable without being logged in, writing relies on the public key alone and therefore cannot be protected by a secret. Integrity is instead provided by sealing, which makes tampering detectable on the next login:

  • On every save performed while a user is logged in, the whole current log is sealed: an RSA-PSS-SHA256 signature (made with the user's private key) is computed over a canonical payload of the sealed entry count, the activity log's public key, and the sealed entry ciphertexts. Verification only needs the public key.
  • The number of sealed entries is anchored inside the encrypted, AEAD-protected database (ActivitySealWatermark). Since that store is tamper-proof, it lets the next login detect a rollback/truncation of the sealed entries, or a stripped signature.
  • On login the log is verified against the private key: the stored public key must match the key pair in the database (defeats a key substitution), and the signature must be valid over the sealed entries (defeats modification, forgery and reordering of the sealed portion).
  • If any check fails, login is not blocked; instead a reviewable ActivityLogTampered activity is recorded so the user is alerted.

Storage format (.pku)

  • A .pku file is a ZIP archive containing four entries: header, database, autosave, and activity. Unexpected entry names, more than four entries, archives larger than 64 MiB, or per-entry stored/decoded payloads above 64 MiB are refused (CorruptedSourceException) so a forged file cannot zip-bomb the process. Activity decrypt also caps at 100,000 entries. Plaintext import files are similarly capped at 64 MiB (ImportFileTooLarge).
  • The header entry holds the sticky KdfParameters (algorithm, iterations, output length, salt). It is not passkey-encrypted — only the shared JSON → GZip → Base64 pipeline applies — because those values must be readable before any key can be derived.
  • Each entry pipeline is: JSON serialize → GZip compress → Base64, then (for database/autosave) the symmetric onion encrypts that compressed payload. The activity entry skips the onion: its records are already protected individually with per-record RSA hybrid encryption before that shared compress/Base64 step. The activity envelope stores only seal metadata and the RSA public key — not a cleartext username (that would be readable without passkeys). Compressing before encryption is deliberate: GZip only shrinks structured plaintext, not high-entropy ciphertext.
  • File access is serialized through a re-entrant lock (FileLocker) to prevent concurrent access races (e.g. a save colliding with the session-timeout timer).
  • Atomic ZIP commits: each entry update builds a complete replacement archive in memory, writes it to a sibling temp file (flushed with write-through), then File.Move(overwrite) swaps it onto the .pku path. Readers therefore see either the previous intact archive or the new one — never a torn ZipArchiveMode.Update rewrite, and never trailing garbage when the archive shrinks. The session handle is released only for that replace and reacquired immediately afterwards. If File.Move still fails after retries (for example a sustained AV lock), Save fails and a complete sibling .tmp archive is left for recovery — the live .pku is not rewritten in place.
  • Deferred persistence: while a user is logged in, autosave and activity-log ZIP rewrites are coalesced with a short debounce (~500 ms) so a burst of field edits becomes a single disk write. Pending work is flushed on explicit Save and on Close. Pre-login events (open, failed login) still write immediately so the audit trail survives a crash before the session starts. The .pku handle is held open for the whole session (FileShare.Read | FileShare.Delete) outside the brief atomic-replace window above; other processes may still open the file for reading, but not for writing. A sibling <vault>.pku.lock opened with FileShare.None enforces that single-writer rule on Linux as well (Unix share modes are advisory). FileShare.Delete is required so the atomic replace can swap the sibling temp file into place.

Static analysis

  • GitHub CodeQL runs on every push (any branch) and weekly. It is not a NuGet dependency of Core, Utils, or Interfaces; it runs on GitHub's infrastructure against a Release build of the production projects (unit tests are omitted from that compilation). Generated bin/obj/*.g.cs findings are filtered from the uploaded SARIF.
  • CI also runs dotnet list … package --vulnerable --include-transitive on the Windows and Linux solutions after restore. Production libraries take no NuGet packages; the audit covers test (and any future) package graphs and fails the job when known vulnerabilities are reported.

Randomness

  • All security-relevant randomness uses the cryptographically secure System.Security.Cryptography.RandomNumberGenerator (keys, salts, nonces, and generated passwords via RandomNumberGenerator.GetInt32). System.Random is never used for secrets.

In-memory hygiene

  • IProtectedSecret / ISecretMemoryProtector: once a vault is unlocked, account passwords, password history, master passkeys, and the RSA private key are held through the protector injected at Create/Open. The Utils default (SecretMemoryProtector → ProtectedSecret) uses AES-256-GCM under a random, process-wide session key. Plaintext is produced only for the duration of Reveal() (display, copy, re-encrypt, or JSON persist into the .pku onion). ToString() never returns the secret (***), so a protected value cannot leak into logs or activity messages by accident. The session key never leaves RAM and dies with the process; a dump of the wrapped blobs after exit is worthless. Persistence still stores plaintext JSON inside the onion-encrypted database / autosave entries — in-memory wrapping is not a second at-rest scheme. Core depends only on the Interfaces ports; hosts may substitute another protector without changing Core.
  • Session undo/redo (IEditHistory): vault field edits, adds, and deletes are recorded in a process-memory stack (capped) for Undo/Redo within the same unlocked session. Entries reuse the same JSON shape as autosave Change payloads, so password and account blobs may be retained as plaintext strings in RAM until logout (same Reveal-into-JSON path as autosave). The stack is never written to the .pku, is cleared on Close / auto-logout, and must not surface Old/New values in UI labels, logs, or activity FieldValue. Master username and passkeys are excluded from the stack. See also "Known Limitations".
  • IDatabase.Login takes a plain string passkey (there is no SecureString overload on the Core API). The WPF GUI keeps the typed secret in PasswordBox.SecurePassword and bridges it through SecureStringExtensions.UseAsString: the unmanaged BSTR is zeroed in a finally block (Marshal.ZeroFreeBSTR) so it only lives for the duration of the Login call. The short-lived managed string passed to Core remains subject to the usual .NET GC limitations documented under "Known Limitations".
  • Owned sensitive buffers are wiped with CryptographicOperations.ZeroMemory after use: derived AES keys, per-layer UTF-8 password bytes used as HKDF IKM, PBKDF2 password/salt/output buffers in GetSlowHash, abandoned onion interlayer plaintext (and encrypt-side ciphertext scratch buffers), and ProtectedSecret unwrap buffers. Final decrypted UTF-8 string results and other managed password strings are not wiped (see Known Limitations).

Progressive login without rollback (online brute-force friction)

  • Logging in is progressive: each call to IDatabase.Login appends one stretched passkey to the in-memory onion stack and attempts decryption. There is no rollback of a wrong attempt. A mistyped passkey permanently poisons the current open session: every subsequent Login call keeps stacking on top of the bad layer, so even the correct remaining passkeys cannot recover the database until the session is closed and the file is reopened from scratch.
  • That behaviour is intentional. Combined with the expensive PBKDF2 stretch on every attempt, it turns an interactive guessing loop into a high-friction path: each wrong guess both costs a full slow-hash and forces a full reopen (and a fresh stretch of every passkey entered so far) before the attacker can try again. It is a deliberate online anti-brute-force layer on top of the offline hardness of the onion encryption — not an accidental UX bug.
  • The legitimate user who mistypes must close the database (or cancel the login UI, which ends the session) and start over. See also the matching note under "Known Limitations".

Session protection

  • Auto-logout: after a configurable inactivity timeout (ISettings.LogoutTimeout), the session is closed automatically and the database file handle is released.
  • Clipboard cleaning: copied passwords are removed from the clipboard (and clipboard history, via the OS-specific IClipboardManager) after a configurable delay (ISettings.CleaningClipboardTimeout). History scrub targets only secrets registered through IUser.RememberClipboardSecret (WPF records auto-cleared copies); it does not reveal every stored password on each tick. The WPF paste hotkeys (Ctrl+Shift+L / Ctrl+Shift+P) go through the same clipboard path before synthesizing Ctrl+V.
  • Service URL storage (Core): IService.Url refuses absolute URIs that are not http/https (same allowlist as import/ServiceUrlHelper). Relative values may still be stored while typing; shell open re-validates.
  • Service URL open (WPF host): opening a service URL uses the OS shell (UseShellExecute). Only absolute http and https URIs are allowed (ServiceUrlHelper). https opens directly; http requires an explicit confirmation dialog. Local paths, UNC paths, and custom schemes are refused so an imported or typed value cannot invoke an unexpected protocol handler. Import stores only the same allowlisted schemes (others become null).
  • Credential confirmation (WPF host): before creating a vault, or before saving a change to username / ordered master passkeys, the WPF client prompts the user to re-type those credentials (CredentialsConfirmationView). On an update that changes credentials, the flow is old credentials first, then new credentials. The same old credentials prompt also runs before vault Delete and before opening User Settings (where plaintext Export lives). Export itself does not re-prompt for credentials (they are already visible in that view) but shows an explicit plaintext warning before writing JSON/CSV. Entry is progressive (username, then each passkey) without rollback, deliberately matching progressive login: a mistype intentionally poisons the in-dialog sequence until Escape resets it (later correct factors cannot recover it — not a UX defect). Closing the dialog cancels the action. Passkey fields use SecurePassword / UseAsString (BSTR wiped) like login; typed factors are not retained in the ViewModel. This is a host-side intentionality / anti-mistype control, not a cryptographic step-up: Core still exposes writable IUser.Username / IUser.Passkeys, and an already-unlocked session can reveal the current passkeys in User settings. Physical access to an unlocked session remains out of scope (see Threat Model).

Password hygiene features

  • Strong password generation uses the CSPRNG over a configurable alphabet. When leak-checking is enabled, generation retries at most five candidates against the HIBP corpus and then gives up (returns empty) rather than hammering the remote service.
  • Leak detection uses two free, no-account k-anonymity providers in order, then an optional machine-local Bloom filter. Primary: the Have I Been Pwned range API (api.pwnedpasswords.com) — only the first 5 characters of the NTLM hash are sent (?mode=ntlm). Failover: XposedOrNot's anonymous password API (passwords.xposedornot.com) — only the first 10 characters of a Keccak-512 hash are sent (raw Keccak, not NIST SHA-3). The password itself never leaves the device. If both remote providers are unreachable and an offline HIBP Bloom filter (.pkbf) is enabled and present, that filter is consulted last: a miss means not leaked (no false negatives); a hit is treated as leaked (conservative — false positives possible at the filter's build-time target rate; default Balanced ~1 %). The filter file is <exe>/pwned-ntlm.pkbf (fixed path in the WPF host — not stored in config.json); a sidecar <filter>.pkbf.ranges holds per-range ETags for incremental updates. If no offline filter is attached, the check fails open (reports "not leaked") so a network problem never blocks the user. These remote calls are the only outbound network traffic; the feature is opt-in per account. Failed remote checks are not cached: only successful answers are kept in process (HIBP ranges by 5-character prefix, XON yes/no by 10-character prefix; both bounded, never persisted). Empty HIBP range bodies (HTTP 200 with no suffixes) are not cached and are treated like an unreachable provider so a truncated or forged empty response cannot pin "not leaked". A full Bloom build that inserts zero hashes is refused (no empty .pkbf published). Requests time out after a few seconds. The GUI and the alert scan use the asynchronous API so the UI thread is not blocked while waiting on the network. The UI does not surface a separate "could not verify" state: a transient failure is expected to succeed on a later attempt, and a lasting failure without a local filter means the machine is offline or both providers are down — cases where nagging the user that a check did not run is not actionable for a local-only tool. The offline filter is application-scoped (shared by all vaults on the machine): enabling or disabling it never deletes the .pkbf; only an explicit delete (App Settings or LeakFilterConfig.TryDeleteFilterFile) removes it, along with its sidecar. Quality preset (LeakFilterConfig.FalsePositiveRate, WPF: LocalLeakDatabaseQuality — Balanced / Strict / Paranoid) is applied only on a full build or rebuild; incremental updates keep the on-disk sizing. LeakFilterConfig.AutoUpdateFrequency (WPF: LocalLeakDatabaseAutoUpdateFrequency, default 7 days; 0 = off) tells the WPF host to refresh an existing .pkbf in the background at startup when offline use is also enabled, the .ranges sidecar is present, and the filter header BuiltUtc is older than that interval; a missing file never triggers an automatic first build, a missing sidecar skips the refresh while keeping the filter, and a corrupt / unusable .pkbf throws HibpBloomCorruptException (logged and skipped — never a silent scratch rebuild). If the WPF UI continues a mid-close refresh in the background, the vault session (and owned clipboard) is ended first. Application logs still live under %LocalAppData%\Passkey\logs — that path is unrelated to the Bloom filter.
  • Duplicate-password and password-update reminder alerts are computed locally.

Known Limitations

These are conscious trade-offs, documented for transparency:

  • Atomic ZIP replace under file locks: on Windows, antivirus or indexers can hold the .pku briefly during the unlocked replace window. Retries usually succeed; if they do not, Save fails rather than rewriting the live file in place. A complete sibling .tmp may remain for manual recovery.
  • Secrets in managed memory: long-lived fields hold IProtectedSecret ciphertext (default Utils ProtectedSecret), not plaintext, which shrinks the window compared to keeping passwords as string for the whole session. Reveal() still returns a .NET string, which is immutable and cannot be reliably zeroed before garbage collection. An attacker able to read process memory or the OS swap file while the database is unlocked — especially during display, clipboard copy, QR encoding, or a save — may recover secrets. This is consistent with the "compromised host" out-of-scope item.
  • Undo/redo stack in RAM: while a vault is unlocked, the session edit history can retain prior field values and full add/delete payloads (including passwords) as JSON strings, in addition to live IProtectedSecret fields and autosave. The stack is capped, session-only, and cleared on close; it is not a durable audit or recovery log.
  • On-screen QR codes: identifiers and passwords can be shown as a QR code (generated in-process, no network). That puts the secret on the display until the window closes or the configured ShowPasswordDelay elapses. Anyone who can see or photograph the screen can capture it.
  • Password stretching algorithm: the project uses PBKDF2 rather than a memory-hard KDF such as Argon2id, because Argon2 is not part of the .NET base class library and the project maintains a zero-external-dependency policy for Core, Utils, Interfaces, and the WPF GUI. To compensate, it uses PBKDF2-HMAC-SHA-512 (more hostile to GPU/ASIC parallelism than SHA-256) with 1,000,000 iterations. The sticky KDF header (see "Crypto-agility") keeps the door open to adopting a memory-hard KDF later, pluggably, should the policy ever be relaxed.
  • Import/Export files: CSV and JSON files produced by the Export feature (and consumed by Import) are unencrypted plaintext by design, for interoperability with other tools — not a substitute for backing up the encrypted .pku. The WPF client warns before writing an export. The .csv path uses JSON-encoded cells and covers services/accounts only; import accepts comma- or tab-delimited rows, while export writes tab-separated rows. .json also carries user settings. Users are responsible for protecting or deleting these files. JSON import refuses settings that would set LogoutTimeout, CleaningClipboardTimeout, or ShowPasswordDelay to 0 (ImportExportError.SecurityTimeoutsDisabled); those controls may still be disabled deliberately in the UI, where security alerts surface the risk.
  • Leak check fails open: if both Have I Been Pwned and XposedOrNot are unreachable (timeout, HTTP error, offline host) and no offline Bloom filter is attached (the .pkbf is absent, or disabled through LeakFilterConfig in the WPF host's config.json), the check reports "not leaked" and the UI stays quiet. Failures are not cached, so a later successful reach of either API can still raise a leak alert. When an offline filter is attached, a Bloom miss is definitive "not leaked"; a Bloom hit is treated as leaked and may include false positives at the build-time quality target (default ~1 %; no false negatives). The residual risk without a local filter is a prolonged outage of both providers during which a password that is in a breach corpus remains unmarked until a check finally completes; that is accepted rather than adding an "unknown / unverified" UI state that the user cannot usefully act on while offline. The two remote corpora are not identical, so a password known only to one provider may be missed when that provider is the one that is down.
  • Leak API response authenticity: successful HTTP bodies from HIBP and XposedOrNot are not authenticated beyond TLS. A network MITM that returns HTTP 200 with empty or forged content could under-report leaks. Empty HIBP ranges are not cached and fall through to XON/Bloom; a full Bloom build with zero inserted hashes is refused. Residual risk of a convincing forged non-empty "clean" answer remains accepted for this local-only tool (no certificate pinning).
  • Unsigned offline Bloom (.pkbf): the filter file beside the executable has no cryptographic authenticity (format checks only). Whoever can replace it on disk can induce false negatives (Bloom miss ⇒ not leaked). That overlaps host compromise / local write access, which is out of scope for vault confidentiality; it is documented so operators do not treat .pkbf as a signed corpus.
  • Offline Bloom filter size / freshness: building the full HIBP-derived .pkbf downloads every range (~1M prefixes), takes hours, and yields a file whose size depends on the quality preset (~2.4 GiB Balanced / ~3.5 GiB Strict / ~4.7 GiB Paranoid). Changing quality requires an explicit full rebuild; HibpBloomBuildMode.Update never changes bit-array sizing. It is a snapshot: new breaches appear in the live APIs first; update when you want the local file to catch up. An update is incremental — ranges are revalidated concurrently with If-None-Match against the ETags in the .pkbf.ranges sidecar (default parallelism 64) and only changed ranges are downloaded and folded in — so freshness costs minutes rather than another full build. A non-zero AutoUpdateFrequency can run that incremental refresh at WPF startup when a .pkbf and sidecar already exist and BuiltUtc is older than the configured number of days; it never starts a first full build automatically, and a missing sidecar skips the refresh while keeping the filter. A corrupt / unusable .pkbf on Update throws HibpBloomCorruptException (auto-update logs and skips; App Settings surfaces the error) — only an explicit Rebuild replaces the file. The sidecar is a cache, never a source of truth: it is bound to one committed state of one filter file and is rejected whenever that no longer matches, because skipping a range whose bits are absent would mean reporting a leaked password as clean.
  • Unsealed activity-log tail: the activity log is tamper-evident only for the portion sealed at the last login (see "Activity-log integrity"). Entries added since then — including events written while no one is logged in, such as failed logins — are not protected against deletion or alteration by an attacker with write access to the file, because writing them requires no secret. Such an attacker could erase the record of their own access before the legitimate user logs in again. Detecting this fully would require a trusted external log; it is out of scope for a purely local, offline tool. Everything sealed at the last login, however, remains tamper-evident, and a wholesale rollback of the sealed portion is detected via the watermark stored in the encrypted database.
  • No login-attempt rollback: a wrong passkey cannot be undone without closing and reopening the database (see "Progressive login without rollback"). This raises the cost of interactive guessing at the expense of UX: a legitimate mistype also requires a full restart of the login sequence.
  • Compressed size leakage: database/autosave are GZip-compressed before encryption, so the ciphertext length tracks the compressed plaintext size. An observer of the .pku file can therefore infer approximate vault size and, in edge cases, rough compressibility of the JSON. Absolute lengths are already visible for any AEAD ciphertext; this trade-off is accepted for the storage savings on structured data.

Reporting Non-Security Bugs

For regular, non-security bugs and feature requests, please use the normal public GitHub issues.

There aren't any published security advisories