Skip to content

Encryption Architecture

Kyku encrypts sensitive fields in state files using native Web Crypto API — no external dependencies required.

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"
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']
);
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.

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);
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
{
"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.

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>,
};
// 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);
}
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
  • 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