
# deriveJWKFromPassword() / deriveKeyFromPassword()

> Turn a password into an AES key-wrapping key with PBES2, so you can run the slow derivation once and reuse the result.

```ts
import { deriveJWKFromPassword } from "unjwt/jwk";
import { secureRandomBytes } from "unjwt/utils";

const salt = secureRandomBytes(16); // store it next to whatever the key protects

const jwk = await deriveJWKFromPassword("correct horse battery staple", {
  alg: "PBES2-HS256+A128KW",
  salt,
  iterations: 600_000,
});
// {
//   alg: "A128KW",
//   ext: true,
//   k: "_gt-0w5HslIN0K0IUO8oFQ",
//   key_ops: ["wrapKey", "unwrapKey"],
//   kty: "oct",
//   kid: "p6xK5iH9-ZvVJ5L_DEoNRJlszQ7jRZ0O4ytiqxv91VE",
// }
```

A password is not a key. It is short, guessable and not random. PBES2 ([RFC 7518 §4.8](https://www.rfc-editor.org/rfc/rfc7518#section-4.8)) runs it through PBKDF2 many times with a salt, which makes each guess expensive, and the output is an AES Key Wrap key. The same password, salt and count always give the same key.

## Signatures

```ts
function deriveJWKFromPassword<
  Alg extends "PBES2-HS256+A128KW" | "PBES2-HS384+A192KW" | "PBES2-HS512+A256KW",
  Kid extends string | false = string,
>(
  password: string | Uint8Array,
  options: {
    "alg": Alg;
    "salt": string | Uint8Array;
    "iterations": number;
    "keyUsage"?: KeyUsage[];
    "kid"?: Kid;
    // JWK metadata merged onto the result
    "use"?: string;
    "x5c"?: string[];
    "x5t"?: string;
    "x5t#S256"?: string;
    "x5u"?: string;
  },
): Promise<JWK_oct<"A128KW" | "A192KW" | "A256KW">>;

function deriveKeyFromPassword(
  password: string | Uint8Array,
  options: {
    alg: "PBES2-HS256+A128KW" | "PBES2-HS384+A192KW" | "PBES2-HS512+A256KW";
    salt: string | Uint8Array;
    iterations: number;
    keyUsage?: KeyUsage[];
    extractable?: boolean;
    toJWK?: boolean;
  },
): Promise<CryptoKey | JWK_oct>;
```

`deriveKeyFromPassword` returns a `CryptoKey` unless `toJWK: true`, and the type follows the option.

## Options

| Option          | Type                   | Default                    | What it does                                                                                                                                     |
| --------------- | ---------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `alg`           | PBES2 name             | required                   | Picks the PBKDF2 hash and the output key size. Any other value is `ERR_JWK_UNSUPPORTED`.                                                         |
| `salt`          | `string \| Uint8Array` | required                   | At least 8 bytes ([RFC 7518 §4.8.1.1](https://www.rfc-editor.org/rfc/rfc7518#section-4.8.1.1)). A string is UTF-8 encoded and measured as bytes. |
| `iterations`    | `number`               | required                   | An integer of at least 1000 ([RFC 7518 §4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518#section-4.8.1.2)). There is no upper limit here.         |
| `keyUsage`      | `KeyUsage[]`           | `["wrapKey", "unwrapKey"]` | Usages of the derived key.                                                                                                                       |
| `kid`           | `string \| false`      | the RFC 7638 thumbprint    | `deriveJWKFromPassword` only. A string is set verbatim; `false` sets none.                                                                       |
| `use`, `x5c`, … | JWK metadata           | none                       | `deriveJWKFromPassword` only. `alg`, `kty`, `key_ops`, `ext` and key material are reserved (`ERR_INVALID_OPTION`).                               |
| `extractable`   | `boolean`              | `false`                    | `deriveKeyFromPassword` only. Ignored with `toJWK`.                                                                                              |
| `toJWK`         | `boolean`              | `false`                    | `deriveKeyFromPassword` only. Return a `JWK_oct` instead of a `CryptoKey`.                                                                       |

## The algorithms

| `alg`                | PBKDF2 hash | Result `alg` | Key size |
| -------------------- | ----------- | ------------ | -------- |
| `PBES2-HS256+A128KW` | SHA-256     | `A128KW`     | 16 bytes |
| `PBES2-HS384+A192KW` | SHA-384     | `A192KW`     | 24 bytes |
| `PBES2-HS512+A256KW` | SHA-512     | `A256KW`     | 32 bytes |

The derived JWK carries the inner AES-KW algorithm, because that is what the key is: an ordinary `A128KW` key. Passing it to [`encrypt()`](/jwt/jwe/encrypt) produces an `A128KW` token, and no PBKDF2 runs at encrypt or decrypt time:

```ts
import { encrypt, decrypt } from "unjwt/jwe";

const token = await encrypt({ sub: "u1" }, jwk);
// header: { kid: "p6xK5iH9-ZvVJ5L_DEoNRJlszQ7jRZ0O4ytiqxv91VE", alg: "A128KW", enc: "A256GCM", typ: "JWT" }

const { payload } = await decrypt(token, jwk); // { sub: "u1" }
```

## Floors, ceilings and defaults

The values below are the ones the library holds every PBES2 count to.

| Where                                                          | Floor                 | Ceiling                  | Default          |
| -------------------------------------------------------------- | --------------------- | ------------------------ | ---------------- |
| `deriveJWKFromPassword` / `deriveKeyFromPassword` `iterations` | 1000                  | none                     | none, you choose |
| `encrypt()` `p2c`                                              | 1000                  | 600 000                  | 600 000          |
| `encrypt()` `p2s`                                              | 8 bytes               | none                     | 16 random bytes  |
| [`wrapKey()`](/jwk/wrap-unwrap) `p2c`                          | 1000                  | 600 000                  | none, required   |
| `decrypt()` / `unwrapKey()` incoming `p2c`                     | `minIterations`, 1000 | `maxIterations`, 600 000 | none             |

The ceiling on decryption is a defence: a token's `p2c` is chosen by whoever made the token, and an attacker would choose a huge one to burn your CPU. `decrypt()` also caps the PBKDF2 work one call may spend across all attempts (2 400 000 iterations by default, the `limits.pbes2Iterations` option).

For a new deployment, 600 000 iterations with PBES2-HS256 matches the [OWASP guidance for PBKDF2-HMAC-SHA256](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2).

## When to use this, and when to pass the password to `encrypt()`

`encrypt(payload, "password")` and `decrypt(token, "password")` already do PBES2. They pick a fresh random salt per token, write it and the count into the header as `p2s` and `p2c`, and run PBKDF2 on both sides:

```ts
const token = await encrypt({ sub: "u1" }, "correct horse battery staple");
// header: { p2c: 600000, p2s: "DeSZVJt1RgmLLxbpyHJ5sA", alg: "PBES2-HS256+A128KW", enc: "A256GCM", typ: "JWT" }
```

Prefer that when tokens are rare, such as an encrypted export a person unlocks by typing a password. Each token gets its own salt, and nothing but the password needs storing.

Prefer `deriveJWKFromPassword` when the same password protects many tokens, such as a service secret configured as a passphrase. You pay PBKDF2 once at startup instead of on every call. The trade-off is that every token shares one derived key, so the salt and count become configuration you must keep stable: change either and the key changes.

## Salts

The salt makes the same password derive different keys in different places, which defeats precomputed tables. Use random bytes from [`secureRandomBytes()`](/utils/encoding) of 16 bytes or more, and store the salt beside what it protects. It is not secret. A fixed string from configuration also works, and it still separates your deployment from everyone else's.

```ts
await deriveJWKFromPassword("pw", { alg: "PBES2-HS256+A128KW", salt: "abc", iterations: 600_000 });
// JWTError ERR_INVALID_OPTION: "salt" must be 8 or more octets (RFC 7518 §4.8.1.1).
```

::warning
PBKDF2 makes guessing slow. It does not make a weak password strong. A short or common password falls to a dictionary attack at any iteration count.
::

## Errors

| Cause                                                                               | `code`                |
| ----------------------------------------------------------------------------------- | --------------------- |
| An `alg` outside the PBES2 family                                                   | `ERR_JWK_UNSUPPORTED` |
| A salt under 8 bytes                                                                | `ERR_INVALID_OPTION`  |
| `iterations` below 1000, fractional, or not a number                                | `ERR_INVALID_OPTION`  |
| A `password` that is neither a string nor bytes, or `options` that is not an object | `ERR_INVALID_OPTION`  |
| A `kid` that is neither a string nor `false`, or a reserved JWK member              | `ERR_INVALID_OPTION`  |

## See also

- [JWE algorithms](/jwt/jwe/algorithms) for PBES2 next to the other key management choices.
- [wrapKey() / unwrapKey()](/jwk/wrap-unwrap) for PBES2 wrapping with explicit `p2s` and `p2c`.
