Skip to content
Kosh Docs

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:

  1. Vault Encryption — protects the vault metadata using a master password.
  2. 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.


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:

  1. Generate ephemeral private key a.
  2. Compute shared secret S = X25519(a, vaultPublicKey).
  3. Hash it to a 32-byte encryption key:
key := sha256.Sum256(encryptionKey)

This ensures constant-length key material and avoids weak shared secrets.

  • 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)
  • Nonce
  • Secret (ciphertext)

When running:

Terminal window
kosh get <label> <user>

Steps:

  1. Prompt for master password.
  2. Derive unlock key with Argon2id.
  3. Decrypt vault secret (verifies password).
  4. Load credential: (EphemeralPub, Nonce, Cipher).
  5. Recompute shared secret:
shared, _ := curve25519.X25519(vaultPrivateKey, EphemeralPub)
key := sha256.Sum256(shared)
  1. Decrypt using ChaCha20-Poly1305:
secret, err := aead.Open(nil, nonce, cipher, nil)
  1. Copy plaintext password to OS clipboard.

Each credential uses a unique ephemeral ECDH key pair.

Argon2id with 64MB RAM requirement.

ChaCha20-Poly1305 prevents tampering.

Only the vault’s private key is stored, encrypted with the master password.

No external APIs; no key material leaves the device.


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

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)

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.

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.


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.

kosh profile delete goes further: before the vault file is unlinked, it is overwritten with cryptographically random bytes and fsync’d to disk.


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.