Encryption Architecture
Technical documentation highlighting the cryptographic structure and flow used.
Kosh uses a modern, hybrid encryption model combining Argon2id, Curve25519 ECDH, and XChaCha20-Poly1305 AEAD. This design ensures that stored credentials are encrypted securely, with forward secrecy properties for every entry.
Kosh separates encryption into two distinct layers:
- Vault Encryption — protects the vault metadata using a master password.
- Credential Encryption — each stored entry uses an ephemeral asymmetric key pair to derive a unique per-entry symmetric key.
This provides:
- Strong master password protection
- Forward secrecy (one leaked credential key doesn’t compromise others)
- No long-term symmetric key stored on disk
- Modern cryptographic primitives only
This document explains how keys are derived, how secrets are encrypted, and how decryption happens during retrieval.
1. Master Password Derivation (Argon2id)
Section titled “1. Master Password Derivation (Argon2id)”The master password is never used directly. Instead, it is fed into Argon2id, a memory-hard KDF:
func GenerateSymmetricKey(secret, salt []byte) []byte { return argon2.IDKey(secret, salt, keyTime, keyMemory, keyThreads, keyLength)}Parameters:
| Parameter | Value |
|---|---|
| Time cost | 1 |
| Memory cost | 64 MB |
| Threads | 4 |
| Output length | 32 bytes |
Argon2id defends against GPU brute force attacks. The vault stores:
- The salt
- An encrypted “vault secret”
- A nonce
During unlock (kosh add, kosh get, etc.), Kosh regenerates this key and attempts to decrypt the vault secret to verify correctness.
2. Asymmetric Encryption for Credentials (Curve25519)
Section titled “2. Asymmetric Encryption for Credentials (Curve25519)”Each credential uses a fresh ephemeral key pair:
privateKey, publicKey := crypto.GenerateAsymmetricKeyPair()Internally:
func GenerateAsymmetricKeyPair() (privateKey, publicKey []byte) { privateKey = make([]byte, 32) rand.Read(privateKey) publicKey, _ = curve25519.X25519(privateKey, curve25519.Basepoint) return privateKey, publicKey}The vault stores a long-term Curve25519 public key. To encrypt a credential:
- Generate ephemeral private key
a. - Compute shared secret
S = X25519(a, vaultPublicKey). - Hash it to a 32-byte encryption key:
key := sha256.Sum256(encryptionKey)This ensures constant-length key material and avoids weak shared secrets.
Why ephemeral keys?
Section titled “Why ephemeral keys?”- Every credential gets a unique key.
- Compromise of one credential does not leak others.
- Vault’s private key is stored encrypted by the master password.
3. Symmetric Encryption (ChaCha20-Poly1305)
Section titled “3. Symmetric Encryption (ChaCha20-Poly1305)”After deriving the per-credential key:
cipher, nonce := crypto.EncryptSecret(key[:], []byte(secret))Under the hood:
aead, _ := chacha20poly1305.NewX(key)nonce := random(aead.NonceSize())cipher := aead.Seal(nil, nonce, secret, nil)Properties:
- AEAD: encryption + authentication
- 192-bit nonce (extended variant via
NewX) - Safe for random nonces
Stored fields per credential:
Ephemeral(public key)NonceSecret(ciphertext)
4. Decrypting a Credential
Section titled “4. Decrypting a Credential”When running:
kosh get <label> <user>Steps:
- Prompt for master password.
- Derive unlock key with Argon2id.
- Decrypt vault secret (verifies password).
- Load credential:
(EphemeralPub, Nonce, Cipher). - Recompute shared secret:
shared, _ := curve25519.X25519(vaultPrivateKey, EphemeralPub)key := sha256.Sum256(shared)- Decrypt using ChaCha20-Poly1305:
secret, err := aead.Open(nil, nonce, cipher, nil)- Copy plaintext password to OS clipboard.
5. Security Properties
Section titled “5. Security Properties”Forward Secrecy
Section titled “Forward Secrecy”Each credential uses a unique ephemeral ECDH key pair.
Password Guessing Resistance
Section titled “Password Guessing Resistance”Argon2id with 64MB RAM requirement.
Authenticated Encryption
Section titled “Authenticated Encryption”ChaCha20-Poly1305 prevents tampering.
No Long-Term Symmetric Keys
Section titled “No Long-Term Symmetric Keys”Only the vault’s private key is stored, encrypted with the master password.
Offline Always
Section titled “Offline Always”No external APIs; no key material leaves the device.
6. Stored Encryption Metadata Summary
Section titled “6. Stored Encryption Metadata Summary”| Layer | Stored Data | Purpose |
|---|---|---|
| Vault | Salt, encrypted Secret, Nonce, vault public key |
Master password verification & vault unlocking |
| Credential | Ephemeral, Nonce, Secret |
Per-credential encryption using ephemeral ECDH |
7. Threat Model Summary
Section titled “7. Threat Model Summary”Kosh assumes:
- Attacker has full access to the vault file
- Attacker cannot guess master password within feasible time
- System clipboard is trusted (OS-level)
- User’s runtime environment is not compromised (no keyloggers or debugging injection)
8. Profile Isolation
Section titled “8. Profile Isolation”Each profile is a fully independent cryptographic domain:
| Per profile | |
|---|---|
| Vault file | ~/.kosh/profiles/<name>.db |
| Master password | independent — there is no global password |
| Argon2id salt | independently random |
| Curve25519 keypair | independently generated |
Unlocking one profile grants no access to another. There is no key hierarchy, no shared root
secret, and no cross-profile derivation. Compromising the personal profile’s master password
reveals nothing about work.
Cross-profile copy asymmetry
Section titled “Cross-profile copy asymmetry”kosh copy moves a credential into another profile by decrypting it with the source profile’s
private key and re-encrypting it under the target profile’s public key, with a fresh ephemeral
keypair and nonce.
Public-key encryption is all that writing requires, so the target profile’s master password is never requested. The consequence is worth stating plainly:
Anyone who can unlock the source profile can add entries to a target profile without knowing its password. They still cannot read anything in the target profile.
This is a write-only capability. It cannot be used to exfiltrate a target profile’s contents.
9. Secure Deletion
Section titled “9. Secure Deletion”Credentials
Section titled “Credentials”The vault database is opened with PRAGMA secure_delete=ON, so a deleted row’s storage is
overwritten within the database file rather than merely unlinked from the index.
Profiles
Section titled “Profiles”kosh profile delete goes further: before the vault file is unlinked, it is overwritten with
cryptographically random bytes and fsync’d to disk.
10. Operational Hardening
Section titled “10. Operational Hardening”Beyond the cryptography itself:
Constant-time comparison. Master-password and secret confirmation prompts compare using
crypto/subtle, so a mismatch reveals nothing through timing.
Redaction by construction. Credential, CredentialData and CredentialSummary implement
slog.LogValuer and render their sensitive fields as [REDACTED]. A debug log physically cannot
print ciphertext, nonces or ephemeral keys — the guarantee is structural, not a matter of remembering
to omit them at each call site.
Plaintext lifetime. Decrypted secrets are handled as []byte end to end rather than being
converted to an immutable string, so fewer copies of plaintext linger in memory awaiting garbage
collection.
Terminal state on interrupt. Ctrl+C during a password prompt restores the terminal (echo back
on) and exits with code 130, instead of leaving the shell in a broken state.
No secrets in argv. Every secret is collected through an interactive prompt, so nothing sensitive appears in shell history or the process table.
Clipboard, not stdout. Retrieved secrets are only ever written to the system clipboard. Kosh does not clear the clipboard afterwards — that remains the user’s responsibility.