Cryptography Engineering

How to Derive AES Encryption Keys from Passwords (PBKDF2)

March 29, 2026
9 Min Read

If you are building an end-to-end encrypted application, you cannot just pass a user's password directly into an encryption algorithm like AES-256-GCM. AES requires exactly 256 bits of highly randomized, uniform data to act as a key. Humans type passwords like apple123, which are short, predictable, and easily brute-forced.

To bridge the gap between human-readable passwords and military-grade encryption keys, we must use a Key Derivation Function (KDF). In this tutorial, we will use the native Web Crypto API to implement PBKDF2 (Password-Based Key Derivation Function 2) entirely in the browser.

The Role of Salts and Iterations

PBKDF2 protects against brute-force and rainbow table attacks using two critical mechanisms:

  • Cryptographic Salt: A random string of data appended to the password before hashing. This ensures that even if two users choose the exact same password, their resulting encryption keys will be completely different.
  • Iteration Count: PBKDF2 runs the hashing algorithm (like SHA-256) over and over again. By setting the iterations to 100,000 or more, we intentionally make the process computationally expensive. It takes the browser a fraction of a second to compute it once, but it would take an attacker centuries to compute it billions of times.

Step 1: Preparing the Raw Materials

The Web Crypto API only accepts ArrayBuffer objects. First, we need to convert the user's password string into a buffer, and generate a 16-byte random salt.

// 1. Encode the password string into a Uint8Array
const passwordString = "mySuperSecretPIN99!";
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(passwordString);

// 2. Generate a 16-byte cryptographically secure random salt
const salt = window.crypto.getRandomValues(new Uint8Array(16));

Step 2: Importing the Key Material

Before we can stretch the password, we must import it into the Web Crypto API as raw key material. Note that this is not an encryption key yet; it is just the raw material we will feed into PBKDF2.

async function importPasswordKey(passwordBuffer) {
    return await window.crypto.subtle.importKey(
        "raw",                 // Format of the key
        passwordBuffer,        // The encoded password
        { name: "PBKDF2" },    // Algorithm to use it with
        false,                 // Not extractable
        ["deriveKey"]          // What we will do with this key
    );
}

Step 3: Deriving the AES-256 Key

Now we execute the heavy lifting. We take the imported password material, apply the salt, and run it through 100,000 iterations of SHA-256. The output will be a perfect, 256-bit AES-GCM encryption key.

async function deriveAESKey(keyMaterial, salt) {
    return await window.crypto.subtle.deriveKey(
        {
            name: "PBKDF2",
            salt: salt,
            iterations: 100000, // Make brute-forcing mathematically unfeasible
            hash: "SHA-256"
        },
        keyMaterial,
        { 
            name: "AES-GCM", 
            length: 256 // We want a 256-bit key output
        },
        false, // Do not allow the raw key to be exported
        ["encrypt", "decrypt"]
    );
}

The Complete Workflow

Putting it all together, here is the complete wrapper function. When a user creates a secure payload, you run their PIN through this function to get the key used for encryption.

async function getEncryptionKeyFromPIN(pinString) {
    // 1. Setup
    const encoder = new TextEncoder();
    const pinBuffer = encoder.encode(pinString);
    const salt = window.crypto.getRandomValues(new Uint8Array(16));

    // 2. Import & Derive
    const keyMaterial = await window.crypto.subtle.importKey(
        "raw", pinBuffer, { name: "PBKDF2" }, false, ["deriveKey"]
    );
    
    const aesKey = await window.crypto.subtle.deriveKey(
        { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" },
        keyMaterial,
        { name: "AES-GCM", length: 256 },
        false,
        ["encrypt", "decrypt"]
    );

    // You MUST save the salt alongside the ciphertext in your database.
    // The recipient will need the exact same salt to reverse the process.
    return { aesKey, salt };
}

Crucial Detail: The salt is not a secret. It must be stored in plaintext in your database alongside the encrypted payload. When the recipient enters the PIN, you must pull that exact salt from the database and feed it into the exact same PBKDF2 function to derive the matching decryption key.

See PBKDF2 in Production

We implemented this exact PBKDF2 architecture in ZeroKey to allow users to lock their burn-after-reading payloads with a custom PIN.

The heavy computational stretching happens instantly on the client's GPU/CPU, ensuring that the backend server never sees the raw PIN or the derived AES key.

Conclusion

Key derivation is the unsung hero of applied cryptography. By leveraging PBKDF2 through the native Web Crypto API, you can confidently take human-readable inputs and stretch them into unbreakable, mathematically random AES-256 keys directly in the browser, leaving no attack vector for your backend.