Encryption Architecture
Kyku encrypts sensitive fields in state files using native Web Crypto API — no external dependencies required.
Architecture
Section titled “Architecture”Passphrase (user-provided) │ ▼PBKDF2 (600,000 iterations, SHA-256, random 16-byte salt) │ ▼256-bit AES key │ ▼AES-256-GCM (random 12-byte IV per encryption) │ ▼Ciphertext + 16-byte auth tag │ ▼Stored as "ENC:base64-ciphertext:base64-tag"Key Derivation (PBKDF2)
Section titled “Key Derivation (PBKDF2)”const salt = crypto.getRandomValues(new Uint8Array(16));const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, ['deriveKey']);
const aesKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256', }, key, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);Parameters
Section titled “Parameters”| Parameter | Value |
|---|---|
| Algorithm | PBKDF2 |
| Hash | SHA-256 |
| Iterations | 600,000 |
| Derived key length | 256 bits |
Why PBKDF2? The original design used Argon2id via
Bun.password.hash(), but that returns a formatted hash string (not raw key bytes) unsuitable for AES key derivation. PBKDF2 is a NIST-standard KDF natively available in Web Crypto, making it the correct tool for this purpose.
Encryption (AES-256-GCM)
Section titled “Encryption (AES-256-GCM)”const iv = crypto.getRandomValues(new Uint8Array(12));const plaintext = new TextEncoder().encode(value);
const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv, tagLength: 128 }, aesKey, plaintext);
// encrypted = ciphertext + auth tag (appended)const ciphertext = encrypted.slice(0, -16);const tag = encrypted.slice(-16);Parameters
Section titled “Parameters”| Parameter | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key size | 256 bits |
| IV | 12 bytes (random, unique per encryption) |
| Auth tag | 16 bytes |
| Output format | ENC:base64Ciphertext:base64Tag |
State File Metadata
Section titled “State File Metadata”{ "version": "2.0", "encrypted": true, "encryptionMeta": { "algorithm": "aes-256-gcm", "kdf": "pbkdf2", "kdfParams": { "iterations": 600000, "hash": "SHA-256", "salt": "base64-salt" }, "iv": "base64-iv" }, "resources": { "db-123": { "config": { "password": "ENC:base64-ciphertext:base64-tag" } } }}The encryptionMeta block is stored once per state file. All encrypted fields share the same KDF salt (one derivation per file). Each field gets its own random IV.
Field Detection
Section titled “Field Detection”Fields are encrypted automatically if they match these name patterns (case-insensitive):
*password*secret*token*credential
Or if explicitly typed as Secret<T>:
import { Secret } from '@kykucloud/types';
const config = { apiKey: 'sk-...' as Secret<string>,};Decryption Flow
Section titled “Decryption Flow”// During apply, when passphrase is available:function decryptValue(encoded: string, meta: EncryptionMeta): string { const [_, ciphertextB64, tagB64] = encoded.match(/^ENC:(.+):(.+)$/); const salt = base64ToBytes(meta.kdfParams.salt); const iv = base64ToBytes(meta.iv);
const key = deriveKey(passphrase, salt, meta.kdfParams); const plaintext = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, concat(base64ToBytes(ciphertextB64), base64ToBytes(tagB64)) );
return new TextDecoder().decode(plaintext);}Plan Mode vs Apply Mode
Section titled “Plan Mode vs Apply Mode”| Mode | Passphrase | Secret Handling |
|---|---|---|
plan |
Not required | Shows [encrypted] |
apply |
Required | Decrypted for provider calls |
output |
Not required | Shows [redacted] |
output --show-secrets |
Required | Decrypted |
Security Considerations
Section titled “Security Considerations”- Salt reuse: Same salt for all fields in one state file is acceptable — each field has a unique IV
- Passphrase strength: 600,000 PBKDF2 iterations provides reasonable brute-force resistance
- Auth tag verification: GCM authentication tag prevents tampering
- No padding oracle: GCM is an authenticated encryption mode, immune to padding oracle attacks
- Web Crypto API: Uses the browser/JavaScript engine’s native crypto implementation, not a JS library