# Generating keys

> 

unjwt gives you two functions for creating keys:

- [`generateJWK`](#generatejwk) — returns a JWK (or a pair of JWKs). Best for 99% of cases.
- [`generateKey`](#generatekey) — returns a `CryptoKey` (or pair, or raw bytes). Use when you need a non-extractable `CryptoKey` or raw byte output.

Both are in `unjwt/jwk`.

## `generateJWK`

```ts
generateJWK(alg, options?)
```

Always returns a JWK (or pair) with `alg` and `kid` set. The `kid` is a fresh `crypto.randomUUID()` unless you provide one.

```ts [examples.ts]
import { generateJWK } from "unjwt/jwk";

// Symmetric → single JWK
const hmac = await generateJWK("HS256");
// { kty: "oct", k: "...", alg: "HS256", kid: "4c3d-..." }

// Asymmetric → keypair
const { privateKey, publicKey } = await generateJWK("RS256");

// Custom kid
const ec = await generateJWK("ES256", { kid: "ec-signing-2025" });

// Custom RSA modulus
const rsa = await generateJWK("RS256", { modulusLength: 4096, use: "sig" });

// Custom EC curve
const ecdh = await generateJWK("ECDH-ES+A256KW", { namedCurve: "X25519" });

// EdDSA explicit
const ed = await generateJWK("Ed25519");
```

### Options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Applies to
    </th>
    
    <th>
      Default
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        kid
      </code>
    </td>
    
    <td>
      Any
    </td>
    
    <td>
      <code>
        crypto.randomUUID()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        use
      </code>
    </td>
    
    <td>
      Any
    </td>
    
    <td>
      —
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        extractable
      </code>
    </td>
    
    <td>
      Any
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keyUsage
      </code>
    </td>
    
    <td>
      Any
    </td>
    
    <td>
      Derived from <code>
        alg
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        modulusLength
      </code>
    </td>
    
    <td>
      RSA only
    </td>
    
    <td>
      <code>
        2048
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        publicExponent
      </code>
    </td>
    
    <td>
      RSA only
    </td>
    
    <td>
      <code>
        0x010001
      </code>
      
       (65537)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        namedCurve
      </code>
    </td>
    
    <td>
      EC / OKP
    </td>
    
    <td>
      Derived from <code>
        alg
      </code>
      
       (see below)
    </td>
  </tr>
  
  <tr>
    <td>
      Any JWK metadata
    </td>
    
    <td>
      Any
    </td>
    
    <td>
      Merged into the result (<code>
        x5c
      </code>
      
      , etc.)
    </td>
  </tr>
</tbody>
</table>

`alg`, `kty`, `key_ops`, and `ext` are **managed by the library** and cannot be overridden via options — they must match the key material.

<warning>

PBES2 algorithms (`PBES2-*`) aren't generation-time algorithms — they're **key-derivation** algorithms. Passing `"PBES2-HS256+A128KW"` to `generateJWK()` throws. Use [`deriveJWKFromPassword`](/jwk/password-derivation) instead.

</warning>

### Algorithm → output shape

<table>
<thead>
  <tr>
    <th>
      Algorithm
    </th>
    
    <th>
      Returns
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        HS*
      </code>
      
      , <code>
        A*KW
      </code>
      
      , <code>
        A*GCM
      </code>
      
      , <code>
        A*GCMKW
      </code>
    </td>
    
    <td>
      <code>
        JWK_oct<Alg>
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        A128CBC-HS256
      </code>
      
      , <code>
        A192CBC-HS384
      </code>
      
      , <code>
        A256CBC-HS512
      </code>
    </td>
    
    <td>
      <code>
        JWK_oct<Alg>
      </code>
      
       (composite AES+HMAC bytes serialized as one key)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        RS*
      </code>
      
      , <code>
        PS*
      </code>
      
      , <code>
        RSA-OAEP*
      </code>
    </td>
    
    <td>
      <code>
        { privateKey: JWK_RSA_Private<Alg>; publicKey: JWK_RSA_Public<Alg> }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ES*
      </code>
    </td>
    
    <td>
      <code>
        { privateKey: JWK_EC_Private<Alg>; publicKey: JWK_EC_Public<Alg> }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Ed25519
      </code>
      
      , <code>
        EdDSA
      </code>
    </td>
    
    <td>
      <code>
        { privateKey: JWK_OKP_Private<Alg>; publicKey: JWK_OKP_Public<Alg> }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ECDH-ES*
      </code>
    </td>
    
    <td>
      EC pair (default curves) <strong>
        or
      </strong>
      
       OKP pair (for <code>
        X25519
      </code>
      
      ) — see <code>
        JWK_Pair
      </code>
    </td>
  </tr>
</tbody>
</table>

The literal algorithm is preserved in the return type — `generateJWK("HS256")` is `JWK_oct<"HS256">`, `generateJWK("RS256")` is `{ publicKey: JWK_RSA_Public<"RS256">; privateKey: JWK_RSA_Private<"RS256"> }`. This narrowing flows through into [`sign`](/jwt/jws/signing) / [`verify`](/jwt/jws/verifying) / [`encrypt`](/jwt/jwe/encrypting) / [`decrypt`](/jwt/jwe/decrypting), which reject JWKs whose `alg` points at the wrong algorithm family at the type level. For generic variables, `JWK_Pair<Alg>` picks the right pair shape per algorithm (e.g. `JWK_Pair<"ECDH-ES+A256KW">` is an EC pair *or* an OKP pair — whichever the runtime curve produces).

### Curve defaults

For EC/OKP algorithms, the `namedCurve` defaults follow the JOSE registry:

<table>
<thead>
  <tr>
    <th>
      Algorithm
    </th>
    
    <th>
      Default curve
    </th>
    
    <th>
      Allowed curves
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        ES256
      </code>
    </td>
    
    <td>
      <code>
        P-256
      </code>
    </td>
    
    <td>
      <code>
        P-256
      </code>
      
       only
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ES384
      </code>
    </td>
    
    <td>
      <code>
        P-384
      </code>
    </td>
    
    <td>
      <code>
        P-384
      </code>
      
       only
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ES512
      </code>
    </td>
    
    <td>
      <code>
        P-521
      </code>
    </td>
    
    <td>
      <code>
        P-521
      </code>
      
       only
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Ed25519
      </code>
      
      /<code>
        EdDSA
      </code>
    </td>
    
    <td>
      <code>
        Ed25519
      </code>
    </td>
    
    <td>
      <code>
        Ed25519
      </code>
      
      , <code>
        Ed448
      </code>
      
       <em>
        (experimental)
      </em>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ECDH-ES*
      </code>
    </td>
    
    <td>
      <code>
        P-256
      </code>
    </td>
    
    <td>
      <code>
        P-256
      </code>
      
      , <code>
        P-384
      </code>
      
      , <code>
        P-521
      </code>
      
      , <code>
        X25519
      </code>
    </td>
  </tr>
</tbody>
</table>

<tip>

For new ECDH-ES key pairs, `X25519` is the modern choice — constant-time, compact, and not subject to the NIST-curve "is this a valid point?" validation overhead.

</tip>

## `generateKey`

```ts
generateKey(alg, options?)
```

Returns a `CryptoKey`, `CryptoKeyPair`, or `Uint8Array` depending on the algorithm — **without** wrapping it as a JWK. The exact return type is narrowed by `alg` and by `options.toJWK`:

<table>
<thead>
  <tr>
    <th>
      Algorithm family
    </th>
    
    <th>
      <code>
        toJWK: false
      </code>
      
       (default)
    </th>
    
    <th>
      <code>
        toJWK: true
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        HS*
      </code>
      
      , <code>
        A*KW
      </code>
      
      , <code>
        A*GCM
      </code>
      
      , <code>
        A*GCMKW
      </code>
    </td>
    
    <td>
      <code>
        CryptoKey
      </code>
    </td>
    
    <td>
      <code>
        JWK_oct
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        A128CBC-HS256
      </code>
      
      , <code>
        A192CBC-HS384
      </code>
      
      , <code>
        A256CBC-HS512
      </code>
    </td>
    
    <td>
      <code>
        Uint8Array<ArrayBuffer>
      </code>
      
       (composite AES+HMAC bytes)
    </td>
    
    <td>
      <code>
        JWK_oct
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        RS*
      </code>
      
      , <code>
        PS*
      </code>
      
      , <code>
        RSA-OAEP*
      </code>
    </td>
    
    <td>
      <code>
        CryptoKeyPair
      </code>
    </td>
    
    <td>
      <code>
        { privateKey: JWK, publicKey: JWK }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ES*
      </code>
      
      , <code>
        Ed25519
      </code>
      
      , <code>
        EdDSA
      </code>
      
      , <code>
        ECDH-ES*
      </code>
    </td>
    
    <td>
      <code>
        CryptoKeyPair
      </code>
    </td>
    
    <td>
      Same pair shape
    </td>
  </tr>
</tbody>
</table>

### When to use `generateKey`

- **Non-extractable keys** — call `generateKey("HS256", { extractable: false })` to keep the key material inside Web Crypto only. Handy for server-side session-signing keys that shouldn't be exported.
- **Raw bytes for dir / AES-CBC-HS*** — `generateKey("A256GCM")` returns a `CryptoKey` directly usable as a CEK for `alg: "dir"`.
- **Composite AES+HMAC bytes** — `generateKey("A256CBC-HS512")` gives you the 64-byte composite buffer ready for use with `encrypt()` (`alg: "dir"`).

```ts [non-extractable.ts]
import { generateKey } from "unjwt/jwk";

// A CryptoKey that can never be exported as JWK
const sessionKey = await generateKey("HS256", { extractable: false });

// sign() can still use it
await sign({ sub: "u1" }, sessionKey, { alg: "HS256" });
```

### `toJWK: true` vs `generateJWK`

`generateKey("HS256", { toJWK: true })` and `generateJWK("HS256")` both return a `JWK_oct`, but `generateJWK` is the ergonomic path for JWK output: it always sets `kid` and accepts JWK metadata fields in its options. Use `generateKey({ toJWK: true })` only when you need a specific non-extractable setup that happens to serialize as JWK — rare.

## See also

- [Importing & exporting →](/jwk/import-export)
- [JWS algorithms →](/jwt/jws/algorithms) — which signing alg to pick.
- [JWE algorithms →](/jwt/jwe/algorithms) — which encryption alg to pick.
- [Password derivation →](/jwk/password-derivation) — for PBES2.
