# Key wrapping

> 

`encrypt()`/`decrypt()` wrap and unwrap the Content Encryption Key internally. When you need the building blocks — for custom hybrid protocols, custom JWE serializations, or interop with other systems — unjwt exposes them directly:

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

<tip>

**You probably don't need these directly.** `encrypt()` and `decrypt()` handle wrapping for every JWE algorithm. Reach for `wrapKey`/`unwrapKey` only when building a [JSON Serialization variant by hand](/jwt/jwe/multi-recipient), implementing a custom cipher protocol, or testing at the primitive level.

</tip>

## `wrapKey`

```ts
wrapKey(alg, keyToWrap, wrappingKey, options?)
```

<table>
<thead>
  <tr>
    <th>
      Parameter
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Role
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        alg
      </code>
    </td>
    
    <td>
      <code>
        KeyManagementAlgorithm
      </code>
      
       (including <code>
        "dir"
      </code>
      
      )
    </td>
    
    <td>
      How to wrap.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keyToWrap
      </code>
    </td>
    
    <td>
      <code>
        CryptoKey | Uint8Array
      </code>
    </td>
    
    <td>
      The CEK to be wrapped.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wrappingKey
      </code>
    </td>
    
    <td>
      <code>
        WrappingKeyFor<alg>
      </code>
    </td>
    
    <td>
      The recipient's key (or password for PBES2).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options
      </code>
    </td>
    
    <td>
      See <a href="#options">
        below
      </a>
    </td>
    
    <td>
      Per-algorithm extras (IV, salt, ephemeral, etc.).
    </td>
  </tr>
</tbody>
</table>

`WrappingKeyFor<alg>` narrows the `wrappingKey` to the shape legal for the selected algorithm — e.g. `WrappingKeyFor<"A128KW">` is `CryptoKey | JWK_oct<"A128KW">`, `WrappingKeyFor<"RSA-OAEP-256">` is `CryptoKey | JWK_RSA_Public<"RSA-OAEP-256">`, and `WrappingKeyFor<"PBES2-HS256+A128KW">` is `string | Uint8Array | JWK_oct<"PBES2-HS256+A128KW">`. AES-GCMKW additionally accepts the bare `A*GCM` counterpart to match the runtime aliasing rule. `unwrapKey`'s `unwrappingKey` parameter uses the symmetric `UnwrappingKeyFor<alg>` with the `_Private` variants on the asymmetric branches.

The return shape depends on `alg`:

<table>
<thead>
  <tr>
    <th>
      <code>
        alg
      </code>
      
       family
    </th>
    
    <th>
      Returns
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        "dir"
      </code>
      
      , <code>
        "A*KW"
      </code>
      
      , <code>
        "RSA-OAEP*"
      </code>
    </td>
    
    <td>
      <code>
        { encryptedKey: Uint8Array }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        "PBES2-*"
      </code>
    </td>
    
    <td>
      <code>
        { encryptedKey, p2s, p2c }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        "A*GCMKW"
      </code>
    </td>
    
    <td>
      <code>
        { encryptedKey, iv, tag }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        "ECDH-ES"
      </code>
    </td>
    
    <td>
      <code>
        { encryptedKey (empty), epk, apu?, apv? }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        "ECDH-ES+A*KW"
      </code>
    </td>
    
    <td>
      <code>
        { encryptedKey, epk, apu?, apv? }
      </code>
    </td>
  </tr>
</tbody>
</table>

For `ECDH-ES` direct (no key wrap), `encryptedKey` is an **empty Uint8Array** — the derived secret *is* the CEK, so there's nothing to ship (per [RFC 7516 §4.6](https://www.rfc-editor.org/rfc/rfc7516#section-4.6)).

```ts [examples.ts]
// AES Key Wrap
const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);

// RSA-OAEP
const { encryptedKey: ek } = await wrapKey("RSA-OAEP-256", cek, rsaPublicJwk);

// ECDH-ES with key wrap
const { encryptedKey, epk } = await wrapKey("ECDH-ES+A256KW", cek, recipientPublicKey);

// ECDH-ES direct (no wrap — derived secret IS the CEK)
const { epk, apu, apv } = await wrapKey("ECDH-ES", rawCek, recipientPublicKey, {
  ecdh: { enc: "A256GCM" },
});
```

### Options

```ts
interface WrapKeyOptions {
  iv?: Uint8Array; // AES-GCMKW
  p2s?: Uint8Array; // PBES2 salt (defaults: 16 random bytes)
  p2c?: number; // PBES2 iterations (default: 600_000)
  ecdh?: {
    ephemeralKey?: CryptoKey | JWK_EC_Private | CryptoKeyPair;
    partyUInfo?: Uint8Array;
    partyVInfo?: Uint8Array;
    enc?: ContentEncryptionAlgorithm; // required for bare "ECDH-ES"
  };
}
```

## `unwrapKey`

```ts
unwrapKey(alg, wrappedKey, unwrappingKey, options?)
```

Reverses `wrapKey`. Returns a `CryptoKey` by default, or raw bytes with `format: "raw"`:

```ts
// Returns CryptoKey (default)
const cek = await unwrapKey("A256KW", encryptedKey, aesKey);

// Returns Uint8Array
const raw = await unwrapKey("A256KW", encryptedKey, aesKey, { format: "raw" });
```

### Format & CryptoKey import

When `format: "cryptokey"` (default), unjwt imports the unwrapped bytes as a Web Crypto `CryptoKey`:

```ts
const cek = await unwrapKey("A256KW", encryptedKey, aesKey, {
  format: "cryptokey", // default
  unwrappedKeyAlgorithm: { name: "AES-GCM", length: 256 },
  keyUsage: ["encrypt", "decrypt"],
  extractable: false,
});
```

When `format: "raw"`, you get the `Uint8Array` and decide how to use it (pass to [`encrypt()`](/jwt/jwe/encrypting) as a `dir` key, pipe into another cipher, etc.).

### PBES2 iteration bounds

`unwrapKey` enforces the same DoS-protection bounds as [`decrypt()`](/jwt/jwe/decrypting#pbes2-dos-protection):

- `minIterations` — default `1000` ([RFC 7518 §4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518#section-4.8.1.2)).
- `maxIterations` — default `1_000_000`.

Set explicitly if your deployment uses unusual values.

### ECDH-ES inputs

For `ECDH-ES*` unwrap, pass the header fields that were in the original token:

```ts
const cek = await unwrapKey("ECDH-ES+A256KW", encryptedKey, myPrivateKey, {
  epk: tokenHeader.epk,
  apu: tokenHeader.apu, // base64url string or Uint8Array
  apv: tokenHeader.apv,
  enc: tokenHeader.enc, // required for bare "ECDH-ES"
});
```

## Typical use — building a manual multi-recipient envelope

Before [`encryptMulti`](/jwt/jwe/multi-recipient) existed, this was the manual pattern for "one ciphertext, many recipients" — and it's still useful as a teaching example of how the spec composes:

```ts [manual-multi.ts]
import { wrapKey, unwrapKey } from "unjwt/jwk";
import { secureRandomBytes } from "unjwt/utils";
import { encrypt, decrypt } from "unjwt/jwe";

const enc = "A256GCM";
const cek = secureRandomBytes(32);

// 1. Encrypt payload once with the CEK
const ciphertext = await encrypt({ msg: "x" }, cek, { alg: "dir", enc });

// 2. Wrap the CEK per recipient
const wrapped = await Promise.all(
  recipients.map(async ({ publicKey }) => {
    const { encryptedKey, epk } = await wrapKey("ECDH-ES+A256KW", cek, publicKey);
    return { encryptedKey, epk };
  }),
);

// 3. Recipient unwraps their own entry, then decrypts
const mine = wrapped[myIndex];
const myCek = await unwrapKey("ECDH-ES+A256KW", mine.encryptedKey, myPrivateKey, {
  format: "raw",
  epk: mine.epk,
  enc,
});
const { payload } = await decrypt(ciphertext, myCek);
```

In practice, use [`encryptMulti`/`decryptMulti`](/jwt/jwe/multi-recipient) instead — they produce a proper RFC 7516 §7.2 General JSON Serialization with all the envelope fields in the right places.

## See also

- [Multi-recipient →](/jwt/jwe/multi-recipient) — the high-level API.
- [ECDH-ES →](/jwt/jwe/ecdh-es) — including [`deriveSharedSecret`](/jwt/jwe/ecdh-es#derivesharedsecret-the-raw-kdf-step).
- [JWE algorithms →](/jwt/jwe/algorithms).
