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.
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.
Please do not open a public GitHub issue for security vulnerabilities.
Report vulnerabilities privately through either of the following channels:
- GitHub Security Advisories (preferred):
- Go to the repository's Security tab: https://github.com/YassinLokhat/Upsilon.Apps.Passkey/security/advisories
- Click Report a vulnerability and describe the issue.
- Email: [email protected]
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.
In scope
- Confidentiality and integrity of the
.pkudatabase 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").
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.
- 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
headerentry. 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
headerentry of the.pkufile. A database is always reopened — and rewritten — with the exact parameters stored in that header. There is no automatic upgrade toDefaultSlowHashParameterson 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 everyGetSlowHashcall enforce a KDF floor and ceiling viaEnsureSufficientSlowHashParameters: 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 raiseInsufficientKdfParametersExceptionand 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.
- The
databaseandautosavepayloads 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).
- 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).
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
ActivityLogTamperedactivity is recorded so the user is alerted.
- A
.pkufile is a ZIP archive containing four entries:header,database,autosave, andactivity. 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
headerentry holds the stickyKdfParameters(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. Theactivityentry 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.pkupath. Readers therefore see either the previous intact archive or the new one — never a tornZipArchiveMode.Updaterewrite, and never trailing garbage when the archive shrinks. The session handle is released only for that replace and reacquired immediately afterwards. IfFile.Movestill fails after retries (for example a sustained AV lock), Save fails and a complete sibling.tmparchive is left for recovery — the live.pkuis 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
Saveand onClose. Pre-login events (open, failed login) still write immediately so the audit trail survives a crash before the session starts. The.pkuhandle 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.lockopened withFileShare.Noneenforces that single-writer rule on Linux as well (Unix share modes are advisory).FileShare.Deleteis required so the atomic replace can swap the sibling temp file into place.
- 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.csfindings are filtered from the uploaded SARIF. - CI also runs
dotnet list … package --vulnerable --include-transitiveon 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.
- All security-relevant randomness uses the cryptographically secure
System.Security.Cryptography.RandomNumberGenerator(keys, salts, nonces, and generated passwords viaRandomNumberGenerator.GetInt32).System.Randomis never used for secrets.
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 ofReveal()(display, copy, re-encrypt, or JSON persist into the.pkuonion).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-encrypteddatabase/autosaveentries — 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 autosaveChangepayloads, 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 onClose/ auto-logout, and must not surface Old/New values in UI labels, logs, or activityFieldValue. Master username and passkeys are excluded from the stack. See also "Known Limitations". IDatabase.Logintakes a plainstringpasskey (there is noSecureStringoverload on the Core API). The WPF GUI keeps the typed secret inPasswordBox.SecurePasswordand bridges it throughSecureStringExtensions.UseAsString: the unmanaged BSTR is zeroed in afinallyblock (Marshal.ZeroFreeBSTR) so it only lives for the duration of theLogincall. The short-lived managedstringpassed to Core remains subject to the usual .NET GC limitations documented under "Known Limitations".- Owned sensitive buffers are wiped with
CryptographicOperations.ZeroMemoryafter use: derived AES keys, per-layer UTF-8 password bytes used as HKDF IKM, PBKDF2 password/salt/output buffers inGetSlowHash, abandoned onion interlayer plaintext (and encrypt-side ciphertext scratch buffers), andProtectedSecretunwrap buffers. Final decrypted UTF-8stringresults and other managed password strings are not wiped (see Known Limitations).
- Logging in is progressive: each call to
IDatabase.Loginappends 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 subsequentLogincall 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".
- 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 throughIUser.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.Urlrefuses absolute URIs that are nothttp/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 absolutehttpandhttpsURIs are allowed (ServiceUrlHelper).httpsopens directly;httprequires 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 becomenull). - 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 useSecurePassword/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 writableIUser.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).
- 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 inconfig.json); a sidecar<filter>.pkbf.rangesholds 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.pkbfpublished). 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 orLeakFilterConfig.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.pkbfin the background at startup when offline use is also enabled, the.rangessidecar is present, and the filter headerBuiltUtcis 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.pkbfthrowsHibpBloomCorruptException(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.
These are conscious trade-offs, documented for transparency:
- Atomic ZIP replace under file locks: on Windows, antivirus or indexers can
hold the
.pkubriefly 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.tmpmay remain for manual recovery. - Secrets in managed memory: long-lived fields hold
IProtectedSecretciphertext (default UtilsProtectedSecret), not plaintext, which shrinks the window compared to keeping passwords asstringfor the whole session.Reveal()still returns a .NETstring, 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
IProtectedSecretfields 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
ShowPasswordDelayelapses. 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.csvpath uses JSON-encoded cells and covers services/accounts only; import accepts comma- or tab-delimited rows, while export writes tab-separated rows..jsonalso carries user settings. Users are responsible for protecting or deleting these files. JSON import refuses settings that would setLogoutTimeout,CleaningClipboardTimeout, orShowPasswordDelayto0(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
.pkbfis absent, or disabled throughLeakFilterConfigin the WPF host'sconfig.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.pkbfas a signed corpus. - Offline Bloom filter size / freshness: building the full HIBP-derived
.pkbfdownloads 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.Updatenever 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 withIf-None-Matchagainst the ETags in the.pkbf.rangessidecar (default parallelism 64) and only changed ranges are downloaded and folded in — so freshness costs minutes rather than another full build. A non-zeroAutoUpdateFrequencycan run that incremental refresh at WPF startup when a.pkbfand sidecar already exist andBuiltUtcis 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.pkbfon Update throwsHibpBloomCorruptException(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/autosaveare GZip-compressed before encryption, so the ciphertext length tracks the compressed plaintext size. An observer of the.pkufile 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.
For regular, non-security bugs and feature requests, please use the normal public GitHub issues.