Repository navigation
Vault and Crypto
English · Русский
The cryptography has not been independently audited. It composes established primitives (libsodium, BouncyCastle, Nimbus SRP-6a) rather than inventing any, but the composition has not been reviewed by a third party. See SECURITY.md.
flowchart TD
PW[master password] -->|Argon2id m=64MiB t=3 p=4, salt| MK[masterKey · 256-bit]
MK -->|wrap / unwrap| DK[dataKey · 256-bit random]
MK -->|HKDF branch| AK[authKey → SRP-6a verifier]
DK -->|XChaCha20-Poly1305 + AAD| REC[vault records]
AK --> SRV[(sync server:<br/>verifier + wrapped dataKey + ciphertext)]
-
masterKey exists only in memory, is never serialized, and is wiped (
zeroize()) as soon as it is no longer needed — it is Argon2id output and a heap dump or swap would expose it. - dataKey is random, encrypts every record, and is stored only wrapped under the masterKey. Changing the master password rewraps it; records are not re-encrypted.
- authKey is derived deterministically from the masterKey for sync authentication. The server never receives it — only the SRP-6a verifier.
KDF_MEMORY_MIB is published as a constant because the Vault panel states how the secrets on
screen are protected, and the number the UI shows must not drift from the one in use.
data class VaultRecord(
val id: String, val type: RecordType, val version: Long,
val updatedAt: Long, val deviceId: String, val deleted: Boolean,
val blob: ByteArray, // XChaCha20-Poly1305(dataKey, payload)
)Metadata is plaintext; blob is sealed with AAD bound to all of that metadata (recordAad),
so a record cannot be swapped into another slot and its plaintext metadata cannot be tampered with
without failing the tag check. Blobs sealed before 0.1.3 bound only id‖type; they are upgraded in
place on the first unlock and are not trusted on read or merge until then.
The same structure is the unit of E2E sync: version is a Lamport counter for last-writer-wins,
and deletion is a tombstone rather than physical erasure.
RecordType covers hosts, groups, credentials, known hosts, trusted CAs, snippets, runbooks and
their run logs, tunnels, settings, terminal history, teams and team identities, and trash
snapshots. IDENTITY is legacy — never written, kept so old tombstones still deserialize.
VaultFileBody
├── meta: { formatVersion, salt, wrappedDataKey } ← plaintext
└── records: [ VaultRecord … ] ← blobs encrypted
FileVault (okio, desktop and Android) caches records in memory and rewrites the file atomically
(temp file + atomicMove). Mutators write the new snapshot to disk first and commit it to fields
only on success, so a failed write leaves the cache, meta and dataKey unchanged. An unreadable
file on unlock yields UnlockResult.Corrupted rather than a partial state.
Error convention in VaultCrypto: unwrapDataKey and open return null only on an AEAD tag
failure (wrong password or corrupted ciphertext) — an expected outcome. Structurally invalid input
(wrong key length, a blob with no room for nonce and tag) is a programming error and throws.
vault.transaction { … } runs a read-modify-write under the vault's own reentrant lock. Anything
composite — reordering hosts, merging a remote batch — must go through it, or a change computed
from a stale snapshot overwrites a just-merged update. That is a real defect this rule exists for
(VaultHostStore vs the background merge).
Vault.mergeRemote returns MergeResult(applied, rejected). Rejected are LWW winners whose
blob failed authentication against their claimed metadata — a tampered or replayed record from a
compromised server. They are not stored, the local record survives, and the rejection is surfaced
rather than silently dropped.
Secret-bearing files go through vault/atomicWriteUtf8 — atomic, 0600, never a plain
writeText (which once broke TOFU known_hosts and left world-readable vault files). Exporting a
key to a user-chosen location uses PrivateConfig.writePrivateFile, which hardens the file
without re-permissioning the directory the user picked.
Secret comparison uses vault/constantTimeEquals, and intermediate key copies are zeroed.
VaultBiometrics + BiometricKeyStore (Android: AndroidBiometricKeyStore) hold a
biometric-bound wrapper of the unlock material. The binding strength is reported honestly to the
UI — a device that can only offer a weaker binding says so rather than implying more. Enrolling
again is required after a sync connect, because connecting changes the vault key.
TrashStore keeps a snapshot of a deleted record as its own record type for the 30-day window —
deliberately not a fatter tombstone, so clients that do not know the type keep it verbatim and
never show it. SecurityLog records security moments (password change, biometric enrollment,
secret copies, host key changes) inside the vault.
Protected: theft of data at rest, a curious or compromised sync server, eavesdropping on sync credentials. Not protected: a compromised device, weak master passwords (there is no recovery), malicious remote hosts, and secrets you deliberately send to a cloud AI.
User guide
- Installation
- Getting started
- Hosts and connections
- Vault and credentials
- Terminal
- Files (SFTP)
- Port forwarding
- Remote desktops
- Snippets and runbooks
- Monitoring and guard
- AI assistant
- Sync and devices
- Teams and sharing
- Settings and shortcuts
- Android
- Troubleshooting
Development
- Architecture
- Building and running
- Connections and transports
- Terminal engine
- Vault and crypto
- Sync protocol
- Remote desktop stacks
- UI layer
- Testing and the gate
- Contributing and releases
Русский — оглавление