Keybay security design
This is the concise security model for the V2 implementation. The complete normative decisions, format ownership rules, failure semantics, and milestone gates are in RFC 0001, amended for passkey methods by RFC 0003.
Security objective
Keybay protects local application secrets at rest without inventing a Keybay account or service. Each host application gets one encrypted store. Platform protection is always required, and optional credential methods can make possession of platform-accessible artifacts insufficient to recover the store key.
The design is intentionally one-way:
authenticated or declared host identity
|
v
fixed host profile -> fixed file root + fixed platform-root location
|
v
platform root -> authenticated key package -> random store key
|
v
encrypted manifest + independently encrypted record frames
No public call selects an identity, path, provider, backend, or alternate store. No failure reaches plaintext, process-memory storage, another provider, or pre-V2 data.
Threat model
Keybay is designed to resist:
- disclosure from copied encrypted files, backups, or dotfile repositories;
- other OS users lacking the application’s platform/file authority;
- accidental cross-application namespace collisions;
- corruption, wrong keys, frame substitution, truncation, and malformed input;
- lost updates and split initialization across processes; and
- recovery of a credential-protected store from platform artifacts alone.
It does not claim to resist:
- root/kernel compromise or an attacker injected into the host process;
- a keylogger, screen capture, terminal compromise, or a malicious Keybay binary;
- plaintext disclosure after the application explicitly reads a value;
- deletion by an actor able to modify both provider state and application files;
- rollback to an older complete authentic snapshot; or
- reliable zeroing of every Dart VM, OS, or immutable-string copy.
Ordinary unsandboxed Linux and unentitled macOS have a further limit. Their declared application namespace is not an authorization boundary. A same-user program may claim it and may reach the login credential store and files. A Keybay passphrase or passkey prevents those artifacts alone from yielding the store key; it cannot prevent denial of service by an actor with the authority to delete or replace them.
Security guarantees
These invariants are claims only where the named platform configuration has matching executable or retained qualification evidence.
| Invariant | Guarantee |
|---|---|
KB-INV-001 |
Keybay-managed persistent data artifacts do not contain plaintext record values or passphrases. |
KB-INV-002 |
Copying the encrypted store without its separately platform-protected root is insufficient to recover records; additional methods require any one configured credential as well. |
KB-INV-003 |
Missing identity/key material, unsupported versions, corruption, and authentication failure fail closed without returning plaintext or silently creating replacement state. |
KB-INV-004 |
Process, lock, reboot, reinstall, backup, transfer, and restore behavior matches the documented policy for the qualified platform configuration. |
KB-INV-005 |
Keybay reports only protection properties established by the running platform and never infers hardware backing from an API or provider name. |
KB-INV-006 |
Identity, provider, entitlement, confinement, and storage transitions never select a weaker fallback or present abandoned state as a fresh empty store. |
KB-INV-007 |
Concurrency, interruption, malformed input, and native-boundary stress preserve confidentiality, integrity, bounded resource use, and typed failure behavior. |
KB-INV-008 |
Backup, synchronization, and cross-device transfer behavior matches the documented nonmigration policy on the reference host configuration. |
Cryptographic construction
Each store has a random 256-bit Kstore. Domain-separated HKDF-SHA256 outputs
derive separate manifest and record-frame keys; Kstore is not used directly
as an AEAD key. Authenticated contexts use distinct labels and length-delimited
fields.
The file is one bounded framed snapshot:
bootstrap | platform-sealed key package | record frame... | sealed manifest
The bootstrap contains only version and bounded-length/provider-continuation fields needed before authentication. It cannot choose an application, path, provider, credential item, or destruction target.
The manifest is XChaCha20-Poly1305 authenticated ciphertext. Its plaintext lists canonical record names, serialized-frame lengths, and SHA-256 digests in physical order. Offsets are derived rather than persisted. Each value is a separate XChaCha20-Poly1305 frame whose authenticated data binds the V2 frame context, store ID, key epoch, frame type, and exact record name. The platform package and manifest bind the resolved storage domain and public prefix at their respective authenticated layers.
An ordinary read authenticates the manifest and decrypts only requested record frames. An ordinary write authenticates the source, copies unchanged frames as ciphertext, encrypts changed frames under fresh nonces, writes a complete stage file, fsyncs it, and atomically renames it. Store-key rotation re-encrypts every frame. V2 deliberately has no journal, append log, tombstones, free list, compaction, Merkle tree, or in-place mutation.
All lengths and counts are bounded before allocation. Limits include 4,096 records, 120-byte record names, and 1 MiB record values. Suite 1 retains its 16 MiB total-file cap. Suite 2 reserves up to 128 KiB for its sealed key package outside a 16 MiB budget for the other file bytes, so growing verification metadata cannot lock a full vault. The physical cap is 16 MiB plus 128 KiB; the aggregate plaintext read limit remains 16 MiB.
Key package and additional methods
The platform root seals the authoritative key package. With no additional
method, that package has one platform-only route to Kstore. Adding a method
transactionally replaces it with a package that requires the platform root
and any one configured passphrase or passkey. Methods are alternatives, not
multiple factors required together. The limit is eight methods, including at
most one passphrase.
V2 passphrase profile 1 uses Argon2id v1.3 with 64 MiB, three iterations, four lanes, a random 16-byte salt, and a 32-byte result. Derivations are serialized per isolate so concurrent opens cannot multiply the 64 MiB working set there. Passkeys use the verified repeatable 32-byte material returned by Keypass’s system or hardware route. Biometric/presence UI alone is never key material.
Each method’s credential output derives a private X25519 key through
domain-separated HKDF-SHA256. The package stores only its public key and an
RFC 9180 HPKE envelope of Kstore (X25519 / HKDF-SHA256 /
ChaCha20-Poly1305). No passphrase, PRF output, private key, or reusable
credential-equivalent secret is persisted. The complete package is bound to
the authenticated manifest. A provisional key recovered through a method is
not returned until that manifest and the current package revision are checked.
HPKE lets anyone holding a public key seal a key to it, and the manifest is
keyed by whatever key the envelope yields. So each method also stores a
confirmation: an HMAC, keyed from the same credential output, over a random
vault secret that the package seals under Kstore. Unlock recomputes it after
opening the envelope. A party holding only the platform root cannot carry the
genuine vault secret into a store keyed by a Kstore it chose, nor confirm a
substitute without the credential.
An authenticated session can rotate Kstore and encrypt it to surviving
public keys without prompting their credentials; confirmations exclude Kstore
and the epoch, so they stay valid. Removal rotates all records; the removed
credential plus an old snapshot cannot derive another method’s private key and
open later generations. It does retain the unchanged vault secret, so its holder,
with the platform root and file, could still substitute a store that surviving
methods confirm. Old snapshots themselves remain recoverable with their old
credentials; this is not rollback protection.
Protected stores use bootstrap suite 2 and policy 3 for both passphrases and
passkeys. The retired singleton-passphrase policy 1 and the unconfirmed 0.2.0
methods policy 2 are rejected with unsupportedStoreVersion; open never
migrates, resets, or rewrites them. The platform-only encoding is unchanged. Suite 2 permits a 128 KiB
sealed package while suite 1 retains its 4 KiB cap. Older suite-1-only clients
fail closed on suite 2.
Credential APIs accept mutable bytes. Keybay snapshots caller input synchronously and clears its owned copy after the operation. Sessions clear their owned store key and temporary plaintext on close. These are useful best-effort reductions in lifetime, not a claim that a garbage-collected Dart process can prove complete memory erasure. The pinned X25519 implementation also owns temporary copies that Keybay cannot overwrite.
Identity and provider binding
Application identity, host-profile code, assurance class, and the profile’s stable storage location are committed into one storage domain. Ordinary path-bound profiles use the canonical file root. iOS uses its fixed location relative to the OS-managed application container, whose absolute path can change on update. The binding retains the actual canonical root independently for filesystem validation. The provider address is derived from qualified identity/profile facts, not read from the file. Provider state inside the bootstrap is a short untrusted continuation value meaningful only to that already-selected provider; authentication of the key package decides whether it belongs to the store.
iOS, Android, and entitled macOS obtain OS-bound application identity. Ordinary Dart executables on Linux and unentitled macOS use the declaration in their owning pubspec or an AOT value embedded by the Keybay compile wrapper. Source paths, working directories, executable names, and environment variables are not durable application identity.
Platform protection levels
These documentation labels describe the OS boundary around the platform key before an optional Keybay passphrase. They are not security scores, runtime attestations, hardware ratings or release-qualification results. Every profile uses the same authenticated file encryption.
| Level | Meaning | Profiles |
|---|---|---|
| App-bound key | The OS or sandbox authenticates the application or signed group before granting key access. | iOS, Android, entitled macOS; the separately qualified Flatpak candidate against other sandboxed apps under its documented confinement. Unsandboxed host programs running as the same user can reach a Flatpak’s portal secret through GNOME Keyring. |
| Account-level | The application namespace organizes the store but does not provide a portable OS-enforced application boundary. Another authorized same-user process may reach the platform root. | Unentitled macOS and ordinary Linux desktop. |
App-bound key access does not imply identical file isolation. iOS and Android normally isolate both the application’s file and key; entitled macOS still needs App Sandbox for file isolation. Apple access groups can intentionally include more than one authorized app. Hardware backing is also separate: Android reports a device-specific security level, while generic Apple Keychain items carry no Keybay Secure Enclave claim.
For account-level profiles, a Keybay passphrase prevents the root and encrypted file alone from yielding the store key. It does not authenticate the calling application, change its platform level, prevent deletion, or protect values already returned to a compromised process. The threat model applies to both levels.
Security levels by platform turns these levels into a concrete exposure matrix per platform and attacker.
The platform guides use the same protection summary: key access, file isolation, hardware assurance, passphrase policy and main limitation, followed by setup, recovery and qualification details. Provider behavior is grounded in Apple’s access-group model, Android Keystore and the Secret Service specification.
Platform policy
| Host profile | File | Platform root | Important boundary |
|---|---|---|---|
| iOS | app-private Application Support; backup exclusion verified | exact signed Data Protection Keychain group; non-syncing, WhenUnlockedThisDeviceOnly |
signed-group isolation; no Secure Enclave claim |
| Android 12+ | app-private noBackupFilesDir |
one non-exportable AES-GCM Android Keystore key | package/UID sandbox; StrongBox requested, actual level measured only in qualification |
| entitled macOS | app container when sandboxed, otherwise derived Application Support | exact signed Data Protection Keychain group | entitlement isolates the root; App Sandbox separately determines file isolation |
| unentitled macOS | derived restrictive Application Support directory | one item in the explicit login Keychain | declared namespace and Keychain ACL/login session, not a portable app sandbox |
| ordinary Linux | derived restrictive XDG data directory | one Secret Service item; private runtime lock for first creation | declared namespace; authorized same-user clients may reach the item |
Provider selection is direct and fallback-free. A missing, locked, invalidated, or mismatched root is an error while encrypted state exists. Create is insert-only/adopt-the-winner where the provider permits it. Reset prepares an exact provider cleanup, revokes the live file generation under lock, commits that cleanup, and reports partial failure rather than claiming success.
The Flatpak candidate uses /.flatpak-info identity, the fixed private
<instance-path>/data/keybay-v2 directory, and a domain-separated XDG Secret
Portal root. It rejects continuation tokens and never falls back to raw Secret
Service. Reset removes the encrypted store and staging, retaining nonsecret
coordination locks and the portal-owned application secret; the next open
generates a fresh store key, but an old
complete encrypted backup can restore access. Native Linux evidence with two
installed application IDs is required before claiming qualified isolation.
Snap, Windows, and unsupported provider configurations fail closed.
Native and filesystem boundaries
POSIX paths are opened descriptor-relative with no-follow checks. Keybay fixes all mutable names below a canonical private root, validates file type and permissions, uses advisory locks for mutation, writes private staging files, fsyncs content and directory state, and atomically replaces the live name. Readers retain an open descriptor to one immutable generation.
Apple Keychain operations use Security/CoreFoundation FFI and typed byte data.
Android uses the existing JNI FFI boundary because Android Keystore has no NDK
secret-storage API. Linux uses exact-pinned typed D-Bus calls to Secret Service
or the Secret Portal, never a shell/text protocol. Open, authentication changes,
and reset may invoke trusted OS/provider UI. Record operations and auth.list
never acquire a provider, including on authentication failure. Interaction policy
is passed internally to providers; it adds no public interaction option.
Supply chain and evidence
Runtime dependencies are pinned. CI freezes the reviewed closure, runs crypto vectors and format/adversarial tests, exercises real provider APIs where available, and separates simulator/emulator evidence from claims about physical hardware. A platform/API name alone is not evidence of secure hardware mediation.
The development integration pins Keypass to its reviewed Git commit; its
PointyCastle dependency is used for passkey verification. Keybay’s HPKE uses
the already pinned cryptography primitives and published/independent vectors.
A hosted Keypass release is required before publishing Keybay; the Git pin
does not satisfy the publication gate. Keypass adapter tests and previously
recorded device probes are not end-to-end qualification of a Keybay vault.
The current physical and lifecycle qualification inventory is in device-security-suite.md. The separate Claude review and remediation follow-up have been accepted; see the review record. This is an AI model review, not a human external audit. The qualification report records the current SDK release scope and deferred evidence.