Skip to content

Password-Based Encryption

This recipe follows PKCS#5 / RFC 8018. A key is derived from a password using PBKDF2, then used to encrypt data with AES-GCM. The salt is stored alongside the ciphertext so the same key can be reconstructed for decryption.

Encrypt data with a password:

val password = "correct-horse-battery-staple".encodeToByteArray()
val salt = CryptographyRandom.nextBytes(16)

// --- Derive a 256-bit key from the password ---
val derivedKeyBytes = provider.get(PBKDF2).secretDerivation(
    digest = SHA256,
    iterations = 600_000,
    outputSize = 256.bits,
    salt = salt
).deriveSecretToByteArray(password)

// --- Import the derived bytes as an AES-GCM key and encrypt ---
val aesGcm = provider.get(AES.GCM)
val aesKey = aesGcm.keyDecoder().decodeFromByteArray(AES.Key.Format.RAW, derivedKeyBytes)
val ciphertext = aesKey.cipher().encrypt(plaintext = "Secret data".encodeToByteArray())

// Store: salt + ciphertext (iteration count and digest are application constants)

Decrypt by re-deriving the key from the same password and stored salt:

// --- Retrieve the stored salt and ciphertext ---
val storedSalt: ByteArray = salt
val storedCiphertext: ByteArray = ciphertext

// --- Re-derive the key with the same parameters ---
val restoredKeyBytes = provider.get(PBKDF2).secretDerivation(
    digest = SHA256,
    iterations = 600_000,
    outputSize = 256.bits,
    salt = storedSalt
).deriveSecretToByteArray(password)

// --- Decrypt ---
val decryptionKey = provider.get(AES.GCM)
    .keyDecoder()
    .decodeFromByteArray(AES.Key.Format.RAW, restoredKeyBytes)
val plaintext = decryptionKey.cipher().decrypt(ciphertext = storedCiphertext)
println(plaintext.decodeToString()) // "Secret data"

The salt is not secret, but it must be stored alongside the ciphertext – without the exact same salt, the derived key will be different and decryption will fail. See Key Derivation for guidance on choosing the iteration count and digest.