Blog/How Keystore Encrypts Your Credentials: A Security Deep Dive

How Keystore Encrypts Your Credentials: A Security Deep Dive

Keystore Team··6 min read

How Keystore Encrypts Your Credentials: A Security Deep Dive

GitGuardian's 2024 report found 23.8 million secrets leaked on GitHub --- a 25% year-over-year increase. Among them, 46,441 OpenAI API keys were exposed every month, a 1,212x increase from 2022. When a leaked key leads to an average breach cost of $4.88 million (IBM/Ponemon, 2024), the encryption protecting stored credentials is not an implementation detail. It is the entire point.

This post explains the specific cryptographic decisions behind Keystore's credential encryption: why we chose AES-256-GCM, how we manage nonces, why each credential gets its own derived key, and how the proxy decryption flow keeps plaintext credentials out of your application entirely.

Why AES-256-GCM, Specifically

AES-256-GCM is an Authenticated Encryption with Associated Data (AEAD) scheme. It encrypts data and simultaneously produces an authentication tag that detects any tampering with the ciphertext. If a single bit is modified, decryption fails. This matters because encryption without authentication is dangerously incomplete --- an attacker who cannot read your data can still corrupt it in useful ways.

The "specifically" matters here. AES supports multiple modes of operation, and the industry has learned painful lessons about the wrong ones.

The Case Against CBC

TLS 1.3, finalized in 2018, dropped CBC (Cipher Block Chaining) mode entirely. It uses AEAD ciphers exclusively, with AES-256-GCM as the primary suite. The reason is a decade of attacks that exploited CBC's fundamental design:

  • POODLE (2014) exploited CBC padding in SSL 3.0, allowing attackers to decrypt secure cookies one byte at a time. Google's Bodo Moeller and Thai Duong demonstrated full session hijacking.
  • Lucky Thirteen (2013) exploited timing differences in CBC padding verification in TLS, enabling plaintext recovery through a side-channel attack. The attack was practical against DTLS implementations.
  • BEAST (2011) exploited predictable initialization vectors in CBC under TLS 1.0.

Every one of these attacks targeted the padding scheme that CBC requires. GCM is a stream cipher mode --- it does not use padding, which eliminates this entire attack surface. GCM is also parallelizable, achieving throughput of roughly 6.4 GB/s on processors with AES-NI hardware acceleration. CBC, by contrast, must process blocks sequentially during encryption.

When TLS 1.3 made the deliberate choice to drop CBC, it validated what cryptographers had been arguing for years: authenticated encryption should be the default, not an option.

Nonce Management: The One Thing You Cannot Get Wrong

GCM has a critical requirement: you must never reuse a nonce (the 96-bit initialization vector) with the same key. A single nonce reuse can leak the authentication key and allow an attacker to forge ciphertexts and decrypt messages.

This is not a theoretical concern. In 2016, researchers conducted an Internet-wide scan of HTTPS servers and found 184 servers that reused nonces in their AES-GCM TLS connections. The paper, titled "Nonce-Disrespecting Adversaries," demonstrated that these servers were vulnerable to full plaintext recovery. Among the affected servers were those belonging to major organizations that presumably had competent security teams.

NIST's guidance is explicit: a single key should not encrypt more than approximately 2^32 messages when using random nonces. Beyond that threshold, the birthday bound makes nonce collisions statistically likely.

Keystore addresses this through per-credential key derivation, which we cover next. By ensuring each credential is encrypted under its own unique derived key, the nonce space resets for every credential. A single credential is encrypted once (and re-encrypted only on rotation), so the 2^32 limit is never remotely approached.

For each encryption operation, the 96-bit nonce is generated using crypto.getRandomValues(), the Web Crypto API's cryptographically secure random number generator. The nonce is stored alongside the ciphertext --- it is not secret, but it must be unique.

Per-Credential Key Derivation

Keystore does not encrypt all credentials with a single key. Each credential gets its own derived key, produced by combining a root key with a unique, randomly generated salt:

1
2
derived_key = KDF(root_key, unique_salt)
ciphertext  = AES-256-GCM(derived_key, nonce, plaintext_credential)

The root key is stored in a hardware-backed key management system, separate from the application database. The salt is stored alongside the ciphertext. An attacker who compromises the database gets ciphertext and salts --- without the root key, these are useless.

This design has a specific security property: cross-credential isolation. If an attacker somehow recovers one derived key (through a side-channel attack, memory dump, or other compromise), they cannot decrypt any other credential. Each credential's key derivation is independent. There is no "master decrypt" scenario short of compromising the root key itself.

How This Compares to Alternatives

AES-256-GCM is not the only AEAD cipher worth considering. Two alternatives deserve mention:

AES-GCM-SIV is a nonce-misuse resistant variant. If you accidentally reuse a nonce, it degrades to deterministic encryption (leaking only whether two plaintexts are identical) rather than catastrophically failing. The tradeoff is performance --- roughly 70% the speed of standard GCM. For Keystore, where each credential is encrypted under its own derived key and nonce reuse is structurally prevented, the performance cost is not justified.

XChaCha20-Poly1305 uses a 192-bit nonce, making random nonce collisions negligible even without per-key derivation. It is the preferred cipher in systems like libsodium and is excellent for high-volume encryption where key rotation is infrequent. However, it lacks the ubiquitous hardware acceleration that AES-GCM benefits from and is not natively supported by the Web Crypto API, which is Keystore's cryptographic foundation.

We chose AES-256-GCM because it is the standard AEAD cipher in TLS 1.3, it is hardware-accelerated, it is natively available in every runtime Keystore targets, and our per-credential key derivation eliminates the nonce management risks that would otherwise argue for GCM-SIV or XChaCha20.

The Proxy Decryption Flow

The architectural decision that makes all of this work is that credentials are never decrypted in your application. The Keystore proxy is the only component that ever sees plaintext keys:

1
2
3
4
5
6
7
8
9
Agent Request (with ks_ token)
  --> Proxy validates token (hash comparison, rate limits, budget)
  --> Proxy fetches encrypted credential (ciphertext + nonce + salt)
  --> Proxy derives key from root key + salt
  --> Proxy decrypts credential via AES-256-GCM
  --> Proxy injects credential into upstream provider request
  --> Provider responds
  --> Proxy wipes decrypted credential from memory
  --> Proxy returns response to agent

The decrypted credential exists in proxy memory for the duration of one HTTP request. It is never written to disk, never logged, never returned to the calling agent. The agent operates exclusively with its ks_ token --- a revocable, auditable, budget-constrained reference that carries no provider credentials.

This is the difference between encrypting secrets and architecting them out of reach. Even if an attacker fully compromises your agent runtime, they get a token that can be revoked in seconds. The actual API keys --- the credentials that cost a three-person startup $82,314 when their Gemini key was stolen in February 2026 --- remain behind the proxy boundary, encrypted at rest, decrypted only transiently, and never exposed to the systems most likely to be compromised.

Encryption is necessary. Architecture that minimizes decryption is what makes it sufficient.