# Password derivation

> 

A password is not a cryptographic key — it's a low-entropy secret that a human might actually remember. To turn a password into a key suitable for encryption, unjwt uses **PBKDF2** (Password-Based Key Derivation Function 2, [RFC 8018](https://www.rfc-editor.org/rfc/rfc8018)), the same primitive that powers [PBES2 JWE](/jwt/jwe/algorithms#password-based-pbes2).

```ts
import { deriveJWKFromPassword, deriveKeyFromPassword } from "unjwt/jwk";
```

## When you'd reach for these

`encrypt(payload, "my-password")` already calls PBKDF2 internally — you don't need to derive a key just to encrypt a JWE token.

These functions matter when you want to:

- **Persist** a password-derived key and reuse it for many encrypt/decrypt operations without re-running PBKDF2 each time.
- Use a password-derived key for a **non-JWE purpose** — signing, a custom cipher, HMAC over something else.
- **Inspect** the derived key (e.g. to transmit it alongside the ciphertext in a custom envelope).

## `deriveJWKFromPassword` — the common case

Returns a `JWK_oct` (symmetric JWK) directly, with `alg` baked in:

```ts [derive-jwk.ts]
import { deriveJWKFromPassword } from "unjwt/jwk";
import { secureRandomBytes } from "unjwt/utils";

const salt = secureRandomBytes(16); // MUST be random, MUST be unique per password
const jwk = await deriveJWKFromPassword("my-strong-password", "PBES2-HS256+A128KW", {
  salt,
  iterations: 600_000, // OWASP-recommended default
  kid: "derived-key",
});
// jwk.kty === "oct", jwk.alg === "A128KW" (after PBES2 unwrap),
// jwk.k contains the derived 128-bit material
```

### Options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Required?
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        salt
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Binds derivations to a specific context.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        iterations
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Higher = more work for attackers <em>
        and
      </em>
      
       you.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        extractable
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Web Crypto <code>
        extractable
      </code>
      
       flag.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keyUsage
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Derived from the AES-KW <code>
        alg
      </code>
    </td>
    
    <td>
      Web Crypto <code>
        KeyUsage[]
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      JWK metadata
    </td>
    
    <td>
      No
    </td>
    
    <td>
      —
    </td>
    
    <td>
      <code>
        kid
      </code>
      
      , <code>
        use
      </code>
      
      , <code>
        x5c
      </code>
      
      , etc. merged in.
    </td>
  </tr>
</tbody>
</table>

`alg`, `kty`, `key_ops`, and `ext` are managed by the library.

## `deriveKeyFromPassword` — raw CryptoKey output

```ts [derive-cryptokey.ts]
import { deriveKeyFromPassword } from "unjwt/jwk";

const cryptoKey = await deriveKeyFromPassword("my-password", "PBES2-HS256+A128KW", {
  salt,
  iterations: 600_000,
});
// CryptoKey suitable for direct use with Web Crypto APIs
```

Pass `toJWK: true` to get a JWK back instead (in which case `deriveJWKFromPassword` is shorter).

## What the `alg` parameter means here

PBES2 names are composite: `PBES2-HS256+A128KW` means:

- **PBKDF2** with **HMAC-SHA256** as its PRF, producing
- an **AES-128 Key Wrap** key.

Only these three values are valid for password derivation:

<table>
<thead>
  <tr>
    <th>
      <code>
        alg
      </code>
    </th>
    
    <th>
      PRF
    </th>
    
    <th>
      Output alg
    </th>
    
    <th>
      Output key size
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        PBES2-HS256+A128KW
      </code>
    </td>
    
    <td>
      HMAC-SHA256
    </td>
    
    <td>
      <code>
        A128KW
      </code>
    </td>
    
    <td>
      16 bytes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        PBES2-HS384+A192KW
      </code>
    </td>
    
    <td>
      HMAC-SHA384
    </td>
    
    <td>
      <code>
        A192KW
      </code>
    </td>
    
    <td>
      24 bytes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        PBES2-HS512+A256KW
      </code>
    </td>
    
    <td>
      HMAC-SHA512
    </td>
    
    <td>
      <code>
        A256KW
      </code>
    </td>
    
    <td>
      32 bytes
    </td>
  </tr>
</tbody>
</table>

## Iteration count — what to pick

- **Default:** `600_000` — current [OWASP recommendation](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) for PBKDF2-HMAC-SHA256.
- **Minimum safe:** `1000` per [RFC 7518 §4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518#section-4.8.1.2), but that's a floor from 2015 — don't ship that in 2026.
- **Legacy interop:** you might see 10 000–100 000 from older systems. Document it; plan to migrate.

<warning>

Password-derived keys are only as strong as the **password**. PBKDF2 makes brute-forcing *expensive*, not *impossible*. A 6-character password is guessable in minutes regardless of iteration count.

</warning>

## Salt — why it must be random

The salt prevents:

- **Rainbow tables** (precomputed derivations of common passwords).
- **Cross-use detection** — without salt, "same password" derivations would be identical across users, leaking information.

Rules:

<steps level="4">

#### **Random**, via `crypto.getRandomValues` (or [`secureRandomBytes()`](/utilities#securerandombytes)).

#### **Unique** per derivation — never reuse a salt across passwords.

#### **≥ 16 bytes** (128 bits). Larger doesn't hurt.

#### Store/transmit the salt alongside the ciphertext — it's not secret.

</steps>

PBES2 JWE tokens carry their salt in the `p2s` header field automatically. You only manage salts manually when using `deriveJWKFromPassword` outside the PBES2 flow.

## See also

- [JWE algorithms → PBES2](/jwt/jwe/algorithms#password-based-pbes2)
- [Generating keys →](/jwk/generating)
