
# wrapKey() / unwrapKey()

> Encrypt one key under another, and get it back. This is the key management step inside every JWE, exposed for custom protocols.

```ts
import { generateJWK, wrapKey, unwrapKey } from "unjwt/jwk";
import { secureRandomBytes } from "unjwt/utils";

const cek = secureRandomBytes(32); // the key that will encrypt your data
const kek = await generateJWK("A256KW"); // the key that protects it

const { encryptedKey } = await wrapKey(cek, kek, { alg: "A256KW" });
// Uint8Array(40)

const back = await unwrapKey(encryptedKey, kek, { alg: "A256KW", format: "raw" });
// Uint8Array(32), the same bytes as cek
```

A JWE encrypts its payload with a random content encryption key (CEK), then protects the CEK with the recipient's key. [`encrypt()`](/jwt/jwe/encrypt) and [`decrypt()`](/jwt/jwe/decrypt) do both steps for you. Call `wrapKey` and `unwrapKey` directly only when you build an envelope of your own, such as storing a per-record data key wrapped under a master key.

## Signatures

```ts
function wrapKey(
  keyToWrap: CryptoKey | Uint8Array,
  wrappingKey: CryptoKey | JWK | Uint8Array | string,
  options: WrapKeyOptions,
): Promise<WrapKeyResult<typeof options.alg>>;

function unwrapKey(
  wrappedKey: Uint8Array,
  unwrappingKey: CryptoKey | JWK | Uint8Array | string,
  options: UnwrapKeyOptions,
): Promise<CryptoKey | Uint8Array>;
```

A string key is a PBES2 password and is refused under any other `alg`.

## Options

`wrapKey`:

| Option                               | Type                     | Default            | What it does                                                                     |
| ------------------------------------ | ------------------------ | ------------------ | -------------------------------------------------------------------------------- |
| `alg`                                | `KeyManagementAlgorithm` | required           | `A*KW`, `A*GCMKW`, `RSA-OAEP*`, `ECDH-ES*`, `PBES2-*` or `dir`.                  |
| `p2s`, `p2c`                         | `Uint8Array`, `number`   | required for PBES2 | Salt of 8 or more bytes; count between 1000 and 600 000.                         |
| `iv`                                 | `Uint8Array`             | random             | AES-GCMKW. For tests and known vectors only; never reuse one under the same key. |
| `ecdh.enc`                           | content algorithm        | none               | Required for bare `ECDH-ES`, where the derived secret is the CEK.                |
| `ecdh.ephemeralKey`                  | key, pair or private JWK | generated          | The sender's one-time key, on the recipient's curve.                             |
| `ecdh.partyUInfo`, `ecdh.partyVInfo` | `Uint8Array`             | empty              | Agreement info mixed into the derivation, at most 1024 bytes each.               |

`unwrapKey`:

| Option                                             | Type                                   | Default                                     | What it does                                                |
| -------------------------------------------------- | -------------------------------------- | ------------------------------------------- | ----------------------------------------------------------- |
| `alg`                                              | `KeyManagementAlgorithm`               | required                                    | The algorithm the key was wrapped with.                     |
| `format`                                           | `"cryptokey" \| "raw"`                 | `"cryptokey"`                               | Return a `CryptoKey` or the raw bytes.                      |
| `iv`, `tag`                                        | base64url or bytes                     | none                                        | Required for AES-GCMKW.                                     |
| `p2s`, `p2c`                                       | base64url or bytes, `number`           | none                                        | Required for PBES2.                                         |
| `minIterations`, `maxIterations`                   | `number`                               | `1000`, `600_000`                           | Bounds on the incoming `p2c`.                               |
| `epk`, `apu`, `apv`                                | JWK or `CryptoKey`, base64url or bytes | none                                        | ECDH-ES. `epk` is required; `apu`/`apv` at most 1024 bytes. |
| `enc`                                              | content algorithm                      | none                                        | Required for bare `ECDH-ES`.                                |
| `unwrappedKeyAlgorithm`, `keyUsage`, `extractable` | Web Crypto params                      | `AES-GCM`, `["encrypt", "decrypt"]`, `true` | How the CEK is imported with `format: "cryptokey"`.         |

## Return shape

`wrapKey`'s result depends on the algorithm. String members are base64url, the form they take in a JWE header.

| `alg`                     | Result                              |
| ------------------------- | ----------------------------------- |
| `A*KW`, `RSA-OAEP*`       | `{ encryptedKey }`                  |
| `dir`                     | `{ encryptedKey }`, always empty    |
| `A*GCMKW`                 | `{ encryptedKey, iv, tag }`         |
| `PBES2-*`                 | `{ encryptedKey, p2s, p2c }`        |
| `ECDH-ES`, `ECDH-ES+A*KW` | `{ encryptedKey, epk, apu?, apv? }` |

`unwrapKey` returns a `CryptoKey` by default and a `Uint8Array` with `format: "raw"`. A literal `format` narrows the type.

## Each algorithm

**AES-KW** wraps a CEK of 16 or more bytes in whole 8-byte blocks. The output is 8 bytes longer than the input.

**AES-GCMKW** encrypts the CEK and returns the `iv` and `tag` the unwrap needs back:

```ts
const kek = await generateJWK("A128GCMKW");
const wrapped = await wrapKey(cek, kek, { alg: "A128GCMKW" });
// { encryptedKey: Uint8Array(32), iv: "C4xH1LIgu0EXKCZt", tag: "MrEWY5UhBM8nkXJLkN_hkA" }

const { iv, tag } = wrapped;
await unwrapKey(wrapped.encryptedKey, kek, { alg: "A128GCMKW", iv, tag, format: "raw" });
```

**RSA-OAEP** encrypts the CEK to the recipient's public key. Only the private key unwraps it.

```ts
const rsa = await generateJWK("RSA-OAEP-256");
const { encryptedKey } = await wrapKey(cek, rsa.publicKey, { alg: "RSA-OAEP-256" });
// Uint8Array(256)
await unwrapKey(encryptedKey, rsa.privateKey, { alg: "RSA-OAEP-256", format: "raw" });
```

**ECDH-ES+A\*KW** agrees on a key-wrapping key with the recipient's public key through a fresh ephemeral key, then wraps the CEK with AES-KW. The ephemeral public key travels as `epk`:

```ts
const ec = await generateJWK("ECDH-ES+A256KW");
const wrapped = await wrapKey(cek, ec.publicKey, { alg: "ECDH-ES+A256KW" });
// {
//   encryptedKey: Uint8Array(40),
//   epk: { kty: "EC", crv: "P-256", x: "53rm0OP0…", y: "mNcYq0yM…" },
// }
const { epk } = wrapped;
await unwrapKey(wrapped.encryptedKey, ec.privateKey, { alg: "ECDH-ES+A256KW", epk, format: "raw" });
```

**ECDH-ES** (bare) wraps nothing. The agreed secret is the CEK itself, so `encryptedKey` is empty and both sides need `enc` to know its length. An X25519 recipient gets an `OKP` ephemeral key:

```ts
const x = await generateJWK("ECDH-ES", { namedCurve: "X25519" });
const agreed = await wrapKey(new Uint8Array(0), x.publicKey, {
  alg: "ECDH-ES",
  ecdh: { enc: "A256GCM", partyUInfo: new TextEncoder().encode("alice") },
});
// {
//   encryptedKey: Uint8Array(0),
//   epk: { kty: "OKP", crv: "X25519", x: "bxu2hvrz…" },
//   apu: "YWxpY2U",
// }
const secret = await unwrapKey(agreed.encryptedKey, x.privateKey, {
  alg: "ECDH-ES",
  epk: agreed.epk,
  apu: agreed.apu,
  enc: "A256GCM",
  format: "raw",
});
// Uint8Array(32)
```

**PBES2** wraps under a key derived from a password. `wrapKey` takes `p2s` and `p2c` explicitly; only `encrypt()` picks defaults for them. See [Password-derived keys](/jwk/password).

```ts
const wrapped = await wrapKey(cek, "correct horse battery staple", {
  alg: "PBES2-HS256+A128KW",
  p2s: secureRandomBytes(16),
  p2c: 600_000,
});
// { encryptedKey: Uint8Array(40), p2c: 600000, p2s: "CobE_SWBITUz0ztyli2Y8w" }
```

**dir** uses the key itself as the CEK. `wrapKey` returns an empty `encryptedKey`.

## Failed unwraps say only that they failed

Every unwrap that runs and does not succeed throws the same `ERR_JWE_DECRYPTION_FAILED` with the message `JWE decryption failed.` A wrong key, a corrupted `encryptedKey` and a forged tag look identical, so an attacker learns nothing from the difference ([RFC 7516 §11.5](https://www.rfc-editor.org/rfc/rfc7516#section-11.5)). Missing parameters are your mistake and say so:

```ts
await unwrapKey(encryptedKey, await generateJWK("A256KW"), { alg: "A256KW" });
// JWTError ERR_JWE_DECRYPTION_FAILED: JWE decryption failed.

await unwrapKey(wrapped.encryptedKey, kek, { alg: "A128GCMKW" });
// JWTError ERR_JWE_INVALID: AES-GCMKW requires 'iv' and 'tag' options for unwrapping
```

## deriveSharedSecret()

The ECDH-ES derivation on its own: an ECDH agreement followed by the Concat KDF ([RFC 7518 §4.6](https://www.rfc-editor.org/rfc/rfc7518#section-4.6)). Both sides derive the same bytes, each from its own private key and the other's public key.

```ts
import { deriveSharedSecret, generateJWK, generateKey } from "unjwt/jwk";

const recipient = await generateJWK("ECDH-ES+A256KW");
const ephemeral = await generateKey("ECDH-ES", { namedCurve: "P-256" });

const senderSide = await deriveSharedSecret(recipient.publicKey, ephemeral.privateKey, {
  enc: "ECDH-ES+A256KW",
});
const recipientSide = await deriveSharedSecret(ephemeral.publicKey, recipient.privateKey, {
  enc: "ECDH-ES+A256KW",
});
// Uint8Array(32), and both are equal
```

```ts
function deriveSharedSecret(
  publicKey: CryptoKey | JWK_EC_Public,
  privateKey: CryptoKey | JWK_EC_Private,
  options: {
    enc: JWK_ECDH_ES | ContentEncryptionAlgorithm; // the KDF's AlgorithmID
    keyLength?: number; // bits; required when enc is "ECDH-ES"
    partyUInfo?: Uint8Array;
    partyVInfo?: Uint8Array;
  },
): Promise<Uint8Array>;
```

The length follows `enc`: 256 bits for `ECDH-ES+A256KW`, 128 for `A128GCM`. With `enc: "ECDH-ES"` there is no length to infer, so `keyLength` is required, a positive multiple of 8 up to 8192.

JWK inputs are imported under `enc` when it is an `ECDH-ES*` name, and under `"ECDH-ES"` when it is a content algorithm. Because a JWK's `alg` is binding, a key generated for `ECDH-ES+A256KW` derives with `enc: "ECDH-ES+A256KW"` and is refused with `enc: "A128GCM"`; a key generated for `"ECDH-ES"` serves the content algorithms.

## Errors

| Cause                                                                                                                                                                                          | `code`                      |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| Any unwrap that runs and fails                                                                                                                                                                 | `ERR_JWE_DECRYPTION_FAILED` |
| Missing `iv`/`tag`, `p2s`/`p2c` or `epk`; an incoming `p2c` outside the bounds; an oversized `apu`/`apv`                                                                                       | `ERR_JWE_INVALID`           |
| A string key under any `alg` but PBES2; a private key to `wrapKey` or a public one to `unwrapKey`; bare `ECDH-ES` without `ecdh.enc`; `deriveSharedSecret` with `"ECDH-ES"` and no `keyLength` | `ERR_JWK_INVALID`           |
| A CEK AES-KW cannot wrap; `p2s` under 8 bytes, `p2c` outside 1000 to 600 000 or a `partyUInfo`/`partyVInfo` over 1024 bytes on wrap; a malformed `keyLength`                                   | `ERR_INVALID_OPTION`        |
| A `wrappedKey` that is not bytes, or `options` that is not an object                                                                                                                           | `ERR_INVALID_OPTION`        |
| An unknown `alg`                                                                                                                                                                               | `ERR_JWK_UNSUPPORTED`       |

## See also

- [JWE algorithms](/jwt/jwe/algorithms) for when to choose each key management algorithm.
- [Multi-recipient JWE](/jwt/jwe/multi), which wraps one CEK for many recipients.
