simple-hpke 0.1.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/util.d.ts CHANGED
@@ -18,7 +18,7 @@ export type RecipientKey = CryptoKey | CryptoKeyPair | Uint8Array | {
18
18
  export declare function aeadSeal(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>;
19
19
  export declare function aeadOpen(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
20
20
  /**
21
- * Seal a fresh (or supplied) AES key to `recipient`, then AES-GCM encrypt a
21
+ * Wrap a fresh (or supplied) AES key to `recipient`, then AES-GCM encrypt a
22
22
  * message under it. The wrapped key, IV, and ciphertext are concatenated
23
23
  * into a single self-describing envelope.
24
24
  *
@@ -49,7 +49,7 @@ export declare function encryptBytes(recipient: RecipientKey, message: Uint8Arra
49
49
  info?: Uint8Array | string;
50
50
  }): Promise<Uint8Array>;
51
51
  /**
52
- * Like `encrypt`, but encodes the envelope bytes to a string — handy for
52
+ * Like `encrypt`, but encodes the envelope bytes to a string -- handy for
53
53
  * transports that carry text (JSON, URLs, headers). Decode with
54
54
  * `fromString(...)` (or any matching decoder) and pass the bytes to
55
55
  * `decrypt` / `decrypt.asString`. Exposed as `encrypt.asString`.
@@ -85,6 +85,37 @@ export declare function encap(pkR: CryptoKey): Promise<{
85
85
  enc: Uint8Array;
86
86
  }>;
87
87
  export declare function decap(enc: Uint8Array, keypair: CryptoKeyPair): Promise<Uint8Array>;
88
+ /**
89
+ * A stable fingerprint for an AES-GCM key: the same key bytes always give
90
+ * the same string, and two different keys effectively never collide.
91
+ *
92
+ * Derived from the key's *behavior* rather than its bytes, so it works on
93
+ * a non-extractable key -- including one rehydrated from IndexedDB, and
94
+ * the keys `create` / `encryptKey` / `open` return by default. The obvious
95
+ * implementation, `exportKey('raw')` then SHA-256, would force every such
96
+ * key to be extractable. HKDF is not an option either: WebCrypto cannot
97
+ * convert an AES-GCM key into an HKDF key, and `deriveBits` only accepts a
98
+ * key imported under HKDF, which needs the raw bytes a non-extractable key
99
+ * withholds. AES-GCM as a PRF is the only primitive such a key still
100
+ * exposes.
101
+ *
102
+ * The derivation is fixed forever (see `src/constants.ts`):
103
+ *
104
+ * ```
105
+ * LABEL = utf8("simple-hpke/keyId/v1")
106
+ * N = SHA-256(LABEL)[0..12] // fixed nonce
107
+ * C = AES-GCM(key, N, LABEL) // ciphertext || 16-byte tag
108
+ * keyId = base64url(SHA-256(LABEL || C))
109
+ * ```
110
+ *
111
+ * @param key An AES-GCM `CryptoKey` with the `encrypt` usage. It does not
112
+ * need to be extractable.
113
+ * @returns The fingerprint: the full 32-byte digest, base64url, unpadded
114
+ * (43 characters).
115
+ * @throws {HpkeError} `ERR_INVALID_AES_KEY` if `key` is not an AES-GCM key
116
+ * or cannot encrypt.
117
+ */
118
+ export declare function keyId(key: CryptoKey): Promise<string>;
88
119
  export declare function i2osp(n: number, len: number): Uint8Array;
89
120
  export declare function concat(...arrays: Uint8Array[]): Uint8Array;
90
121
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,kBAAkB,EAC1B,MAAM,aAAa,CAAA;AAYpB;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAClB,SAAS,GACT,aAAa,GACb,UAAU,GACV;IAAE,SAAS,EAAC,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAAE,CAAA;AAExD;;GAEG;AACH,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,SAAS,EAAC,UAAU,GACtB,OAAO,CAAC,UAAU,CAAC,CAcpB;AAED,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,UAAU,EAAC,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC,CAcpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,YAAY,CAC9B,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAC1B,GACH,OAAO,CAAC,UAAU,CAAC,CA0BpB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,eAAe,CACjC,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAC/B,GACH,OAAO,CAAC,MAAM,CAAC,CAGhB;AAQD,wBAAsB,IAAI,CAAE,GAAG,EAAC,UAAU,EAAE,IAAI,EAAC,UAAU,GAAE,OAAO,CAAC,UAAU,CAAC,CAS/E;AAiCD,wBAAsB,cAAc,CAChC,OAAO,EAAC,UAAU,EAClB,IAAI,EAAC,UAAU,EACf,KAAK,EAAC,MAAM,EACZ,GAAG,EAAC,UAAU,GAChB,OAAO,CAAC,UAAU,CAAC,CAQpB;AAGD,wBAAsB,aAAa,CAC/B,OAAO,EAAC,UAAU,EAClB,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,MAAM,EACZ,IAAI,EAAC,UAAU,EACf,MAAM,EAAC,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CASpB;AA8BD,wBAAsB,yBAAyB,CAC3C,SAAS,EAAC,YAAY,GACxB,OAAO,CAAC,SAAS,CAAC,CAiCnB;AA6DD,wBAAsB,KAAK,CACvB,GAAG,EAAC,SAAS,GACf,OAAO,CAAC;IAAE,YAAY,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,UAAU,CAAA;CAAE,CAAC,CAarD;AAID,wBAAsB,KAAK,CACvB,GAAG,EAAC,UAAU,EACd,OAAO,EAAC,aAAa,GACvB,OAAO,CAAC,UAAU,CAAC,CAiBpB;AAKD,wBAAgB,KAAK,CAAE,CAAC,EAAC,MAAM,EAAE,GAAG,EAAC,MAAM,GAAE,UAAU,CAQtD;AAED,wBAAgB,MAAM,CAAE,GAAG,MAAM,EAAC,UAAU,EAAE,GAAE,UAAU,CAUzD"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,kBAAkB,EAC1B,MAAM,aAAa,CAAA;AAepB;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAClB,SAAS,GACT,aAAa,GACb,UAAU,GACV;IAAE,SAAS,EAAC,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAAE,CAAA;AAExD;;GAEG;AACH,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,SAAS,EAAC,UAAU,GACtB,OAAO,CAAC,UAAU,CAAC,CAcpB;AAED,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,UAAU,EAAC,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC,CA2BpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,YAAY,CAC9B,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAC1B,GACH,OAAO,CAAC,UAAU,CAAC,CAoBpB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,eAAe,CACjC,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAC/B,GACH,OAAO,CAAC,MAAM,CAAC,CAGhB;AAQD,wBAAsB,IAAI,CAAE,GAAG,EAAC,UAAU,EAAE,IAAI,EAAC,UAAU,GAAE,OAAO,CAAC,UAAU,CAAC,CAS/E;AAiCD,wBAAsB,cAAc,CAChC,OAAO,EAAC,UAAU,EAClB,IAAI,EAAC,UAAU,EACf,KAAK,EAAC,MAAM,EACZ,GAAG,EAAC,UAAU,GAChB,OAAO,CAAC,UAAU,CAAC,CAQpB;AAGD,wBAAsB,aAAa,CAC/B,OAAO,EAAC,UAAU,EAClB,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,MAAM,EACZ,IAAI,EAAC,UAAU,EACf,MAAM,EAAC,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CASpB;AA+BD,wBAAsB,yBAAyB,CAC3C,SAAS,EAAC,YAAY,GACxB,OAAO,CAAC,SAAS,CAAC,CAkDnB;AAmED,wBAAsB,KAAK,CACvB,GAAG,EAAC,SAAS,GACf,OAAO,CAAC;IAAE,YAAY,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,UAAU,CAAA;CAAE,CAAC,CAarD;AAID,wBAAsB,KAAK,CACvB,GAAG,EAAC,UAAU,EACd,OAAO,EAAC,aAAa,GACvB,OAAO,CAAC,UAAU,CAAC,CAwBpB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,KAAK,CAAE,GAAG,EAAC,SAAS,GAAE,OAAO,CAAC,MAAM,CAAC,CAuC1D;AAKD,wBAAgB,KAAK,CAAE,CAAC,EAAC,MAAM,EAAE,GAAG,EAAC,MAAM,GAAE,UAAU,CAQtD;AAED,wBAAgB,MAAM,CAAE,GAAG,MAAM,EAAC,UAAU,EAAE,GAAE,UAAU,CAUzD"}
package/dist/util.js CHANGED
@@ -1283,6 +1283,19 @@ function toString2(array, encoding = "utf8") {
1283
1283
  }
1284
1284
  __name(toString2, "toString");
1285
1285
 
1286
+ // src/errors.ts
1287
+ var HpkeError = class extends Error {
1288
+ static {
1289
+ __name(this, "HpkeError");
1290
+ }
1291
+ code;
1292
+ constructor(code2, message, opts) {
1293
+ super(message, opts);
1294
+ this.name = "HpkeError";
1295
+ this.code = code2;
1296
+ }
1297
+ };
1298
+
1286
1299
  // src/constants.ts
1287
1300
  var KEM_ID = 32;
1288
1301
  var KDF_ID = 1;
@@ -1305,19 +1318,11 @@ var HPKE_SUITE_ID = concat2(
1305
1318
  i2osp(KDF_ID, 2),
1306
1319
  i2osp(AEAD_ID, 2)
1307
1320
  );
1321
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1322
+ var KEY_ID_NONCE_LENGTH = 12;
1308
1323
 
1309
1324
  // src/index.ts
1310
1325
  var subtle = globalThis.crypto.subtle;
1311
- async function create2(recipient, opts) {
1312
- const { wrapped, key } = await encryptKey(recipient, null, opts);
1313
- return { enc: wrapped, key };
1314
- }
1315
- __name(create2, "create");
1316
- async function seal(recipient, aesKey, opts) {
1317
- const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1318
- return { enc: wrapped, key };
1319
- }
1320
- __name(seal, "seal");
1321
1326
  var open = Object.assign(openBytes, {
1322
1327
  raw: openRawBytes
1323
1328
  });
@@ -1330,13 +1335,19 @@ var decrypt = Object.assign(decryptBytes, {
1330
1335
  });
1331
1336
  async function decryptBytes(keypair, message, opts) {
1332
1337
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1333
- throw new Error("malformed message: too short");
1338
+ throw new HpkeError(
1339
+ "ERR_MALFORMED_MESSAGE",
1340
+ "malformed message: too short"
1341
+ );
1334
1342
  }
1335
1343
  const wrappedLen = message[0] << 8 | message[1];
1336
1344
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1337
1345
  const ctStart = ivStart + NN;
1338
1346
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1339
- throw new Error("malformed message: too short");
1347
+ throw new HpkeError(
1348
+ "ERR_MALFORMED_MESSAGE",
1349
+ "malformed message: too short"
1350
+ );
1340
1351
  }
1341
1352
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1342
1353
  const iv = message.slice(ivStart, ctStart);
@@ -1347,11 +1358,20 @@ async function decryptBytes(keypair, message, opts) {
1347
1358
  opts?.info !== void 0 ? { info: opts.info } : void 0
1348
1359
  );
1349
1360
  const key = await importAesKeyDecryptOnly(keyBytes);
1350
- const pt = await subtle.decrypt(
1351
- { name: "AES-GCM", iv },
1352
- key,
1353
- ciphertext
1354
- );
1361
+ let pt;
1362
+ try {
1363
+ pt = await subtle.decrypt(
1364
+ { name: "AES-GCM", iv },
1365
+ key,
1366
+ ciphertext
1367
+ );
1368
+ } catch (err) {
1369
+ throw new HpkeError(
1370
+ "ERR_DECRYPT_FAILED",
1371
+ "failed to decrypt the message: tampered ciphertext",
1372
+ { cause: err }
1373
+ );
1374
+ }
1355
1375
  return new Uint8Array(pt);
1356
1376
  }
1357
1377
  __name(decryptBytes, "decryptBytes");
@@ -1372,7 +1392,8 @@ async function decryptFromString(keypair, message, opts) {
1372
1392
  __name(decryptFromString, "decryptFromString");
1373
1393
  function validateKeysize(keysize) {
1374
1394
  if (keysize !== 128 && keysize !== 256) {
1375
- throw new Error(
1395
+ throw new HpkeError(
1396
+ "ERR_INVALID_KEYSIZE",
1376
1397
  `invalid keysize: ${keysize} (expected 128 or 256)`
1377
1398
  );
1378
1399
  }
@@ -1380,7 +1401,8 @@ function validateKeysize(keysize) {
1380
1401
  __name(validateKeysize, "validateKeysize");
1381
1402
  function validateRawKeyBytes(raw) {
1382
1403
  if (raw.length !== 16 && raw.length !== 32) {
1383
- throw new Error(
1404
+ throw new HpkeError(
1405
+ "ERR_INVALID_AES_KEY",
1384
1406
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1385
1407
  );
1386
1408
  }
@@ -1394,19 +1416,20 @@ function normalizeInfo(info) {
1394
1416
  __name(normalizeInfo, "normalizeInfo");
1395
1417
  async function exportAesKeyBytes(key) {
1396
1418
  if (!key.extractable) {
1397
- throw new Error(
1419
+ throw new HpkeError(
1420
+ "ERR_INVALID_AES_KEY",
1398
1421
  "aesKey must be extractable: its raw bytes are what get sealed"
1399
1422
  );
1400
1423
  }
1401
1424
  return new Uint8Array(await subtle.exportKey("raw", key));
1402
1425
  }
1403
1426
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1404
- async function importAesKey(raw) {
1427
+ async function importAesKey(raw, extractable) {
1405
1428
  return subtle.importKey(
1406
1429
  "raw",
1407
1430
  raw,
1408
1431
  { name: "AES-GCM" },
1409
- true,
1432
+ extractable,
1410
1433
  ["encrypt", "decrypt"]
1411
1434
  );
1412
1435
  }
@@ -1461,7 +1484,10 @@ async function keySchedule(sharedSecret, info) {
1461
1484
  __name(keySchedule, "keySchedule");
1462
1485
  async function openRawBytes(keypair, wrapped, opts) {
1463
1486
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1464
- throw new Error("malformed envelope: too short");
1487
+ throw new HpkeError(
1488
+ "ERR_MALFORMED_ENVELOPE",
1489
+ "malformed envelope: too short"
1490
+ );
1465
1491
  }
1466
1492
  const info = normalizeInfo(opts?.info);
1467
1493
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1473,7 +1499,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1473
1499
  __name(openRawBytes, "openRawBytes");
1474
1500
  async function openBytes(keypair, wrapped, opts) {
1475
1501
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1476
- return importAesKey(keyBytes);
1502
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1477
1503
  }
1478
1504
  __name(openBytes, "openBytes");
1479
1505
  async function encryptKey(recipient, aesKey, opts) {
@@ -1497,7 +1523,10 @@ async function encryptKey(recipient, aesKey, opts) {
1497
1523
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1498
1524
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1499
1525
  const wrapped = concat2(enc, ciphertext);
1500
- const aesGcmKey = await importAesKey(keyBytes);
1526
+ const aesGcmKey = await importAesKey(
1527
+ keyBytes,
1528
+ opts?.extractable ?? false
1529
+ );
1501
1530
  return { wrapped, key: aesGcmKey };
1502
1531
  }
1503
1532
  __name(encryptKey, "encryptKey");
@@ -1528,34 +1557,38 @@ async function aeadOpen(key, nonce, ciphertext) {
1528
1557
  false,
1529
1558
  ["decrypt"]
1530
1559
  );
1531
- const pt = await subtle2.decrypt(
1532
- { name: "AES-GCM", iv: nonce },
1533
- k,
1534
- ciphertext
1535
- );
1560
+ let pt;
1561
+ try {
1562
+ pt = await subtle2.decrypt(
1563
+ { name: "AES-GCM", iv: nonce },
1564
+ k,
1565
+ ciphertext
1566
+ );
1567
+ } catch (err) {
1568
+ throw new HpkeError(
1569
+ "ERR_DECRYPT_FAILED",
1570
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1571
+ { cause: err }
1572
+ );
1573
+ }
1536
1574
  return new Uint8Array(pt);
1537
1575
  }
1538
1576
  __name(aeadOpen, "aeadOpen");
1539
1577
  async function encryptBytes(recipient, message, aesKey, opts) {
1540
1578
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1541
- let enc;
1542
- let key;
1543
- if (!aesKey) {
1544
- const keys = await create2(recipient, opts);
1545
- enc = keys.enc;
1546
- key = keys.key;
1547
- } else {
1548
- const keys = await seal(recipient, aesKey, opts);
1549
- enc = keys.enc;
1550
- key = keys.key;
1551
- }
1579
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1552
1580
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1553
1581
  const ct = new Uint8Array(await subtle2.encrypt(
1554
1582
  { name: "AES-GCM", iv },
1555
1583
  key,
1556
1584
  plaintext
1557
1585
  ));
1558
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1586
+ return concat2(
1587
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1588
+ wrapped,
1589
+ iv,
1590
+ ct
1591
+ );
1559
1592
  }
1560
1593
  __name(encryptBytes, "encryptBytes");
1561
1594
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1621,7 +1654,8 @@ async function exportRawPublic(key) {
1621
1654
  __name(exportRawPublic, "exportRawPublic");
1622
1655
  async function importRawPublic(raw) {
1623
1656
  if (raw.length !== ENC_LENGTH) {
1624
- throw new Error(
1657
+ throw new HpkeError(
1658
+ "ERR_INVALID_RECIPIENT_KEY",
1625
1659
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1626
1660
  );
1627
1661
  }
@@ -1640,9 +1674,16 @@ async function resolveRecipientPublicKey(recipient) {
1640
1674
  }
1641
1675
  if (recipient instanceof CryptoKey) {
1642
1676
  if (recipient.type !== "public") {
1643
- throw new Error("recipient CryptoKey must be a public key");
1677
+ throw new HpkeError(
1678
+ "ERR_INVALID_RECIPIENT_KEY",
1679
+ "recipient CryptoKey must be a public key"
1680
+ );
1644
1681
  }
1645
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1682
+ assertX25519Algorithm(
1683
+ recipient,
1684
+ "recipient CryptoKey",
1685
+ "ERR_INVALID_RECIPIENT_KEY"
1686
+ );
1646
1687
  return recipient;
1647
1688
  }
1648
1689
  if (typeof recipient.publicKey === "string") {
@@ -1652,17 +1693,28 @@ async function resolveRecipientPublicKey(recipient) {
1652
1693
  const pair = recipient;
1653
1694
  if (pair.publicKey instanceof CryptoKey) {
1654
1695
  if (pair.publicKey.type !== "public") {
1655
- throw new Error("recipient keypair publicKey must be a public key");
1696
+ throw new HpkeError(
1697
+ "ERR_INVALID_RECIPIENT_KEY",
1698
+ "recipient keypair publicKey must be a public key"
1699
+ );
1656
1700
  }
1657
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1701
+ assertX25519Algorithm(
1702
+ pair.publicKey,
1703
+ "recipient keypair publicKey",
1704
+ "ERR_INVALID_RECIPIENT_KEY"
1705
+ );
1658
1706
  return pair.publicKey;
1659
1707
  }
1660
- throw new Error("unrecognized recipient key form");
1708
+ throw new HpkeError(
1709
+ "ERR_INVALID_RECIPIENT_KEY",
1710
+ "unrecognized recipient key form"
1711
+ );
1661
1712
  }
1662
1713
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1663
- function assertX25519Algorithm(key, context) {
1714
+ function assertX25519Algorithm(key, context, code2) {
1664
1715
  if (key.algorithm.name !== "X25519") {
1665
- throw new Error(
1716
+ throw new HpkeError(
1717
+ code2,
1666
1718
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1667
1719
  );
1668
1720
  }
@@ -1676,7 +1728,8 @@ async function dh(priv, pub) {
1676
1728
  );
1677
1729
  const bytes = new Uint8Array(bits);
1678
1730
  if (bytes.every((b) => b === 0)) {
1679
- throw new Error(
1731
+ throw new HpkeError(
1732
+ "ERR_SMALL_ORDER_KEY",
1680
1733
  "X25519 shared secret is all-zero (small-order public key?)"
1681
1734
  );
1682
1735
  }
@@ -1715,9 +1768,16 @@ async function encap(pkR) {
1715
1768
  __name(encap, "encap");
1716
1769
  async function decap(enc, keypair) {
1717
1770
  if (keypair.privateKey.type !== "private") {
1718
- throw new Error("keypair.privateKey must be a private key");
1771
+ throw new HpkeError(
1772
+ "ERR_INVALID_KEYPAIR",
1773
+ "keypair.privateKey must be a private key"
1774
+ );
1719
1775
  }
1720
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1776
+ assertX25519Algorithm(
1777
+ keypair.privateKey,
1778
+ "keypair.privateKey",
1779
+ "ERR_INVALID_KEYPAIR"
1780
+ );
1721
1781
  const pkE = await subtle2.importKey(
1722
1782
  "raw",
1723
1783
  enc,
@@ -1731,6 +1791,35 @@ async function decap(enc, keypair) {
1731
1791
  return extractAndExpand(dhBytes, kemContext);
1732
1792
  }
1733
1793
  __name(decap, "decap");
1794
+ async function keyId(key) {
1795
+ if (key.algorithm.name !== "AES-GCM") {
1796
+ throw new HpkeError(
1797
+ "ERR_INVALID_AES_KEY",
1798
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1799
+ );
1800
+ }
1801
+ if (!key.usages.includes("encrypt")) {
1802
+ throw new HpkeError(
1803
+ "ERR_INVALID_AES_KEY",
1804
+ "keyId needs a key with the 'encrypt' usage"
1805
+ );
1806
+ }
1807
+ const labelHash = new Uint8Array(
1808
+ await subtle2.digest("SHA-256", KEY_ID_LABEL)
1809
+ );
1810
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1811
+ const ciphertext = new Uint8Array(await subtle2.encrypt(
1812
+ { name: "AES-GCM", iv: nonce },
1813
+ key,
1814
+ KEY_ID_LABEL
1815
+ ));
1816
+ const digest2 = await subtle2.digest(
1817
+ "SHA-256",
1818
+ concat2(KEY_ID_LABEL, ciphertext)
1819
+ );
1820
+ return toString2(new Uint8Array(digest2), "base64url");
1821
+ }
1822
+ __name(keyId, "keyId");
1734
1823
  function i2osp(n, len) {
1735
1824
  const out = new Uint8Array(len);
1736
1825
  let v = n;
@@ -1763,6 +1852,7 @@ export {
1763
1852
  encryptToString,
1764
1853
  hmac,
1765
1854
  i2osp,
1855
+ keyId,
1766
1856
  labeledExpand,
1767
1857
  labeledExtract,
1768
1858
  resolveRecipientPublicKey