simple-hpke 0.2.0 → 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
@@ -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,CAepB;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,6 +1318,8 @@ 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;
@@ -1320,13 +1335,19 @@ var decrypt = Object.assign(decryptBytes, {
1320
1335
  });
1321
1336
  async function decryptBytes(keypair, message, opts) {
1322
1337
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1323
- throw new Error("malformed message: too short");
1338
+ throw new HpkeError(
1339
+ "ERR_MALFORMED_MESSAGE",
1340
+ "malformed message: too short"
1341
+ );
1324
1342
  }
1325
1343
  const wrappedLen = message[0] << 8 | message[1];
1326
1344
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1327
1345
  const ctStart = ivStart + NN;
1328
1346
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1329
- throw new Error("malformed message: too short");
1347
+ throw new HpkeError(
1348
+ "ERR_MALFORMED_MESSAGE",
1349
+ "malformed message: too short"
1350
+ );
1330
1351
  }
1331
1352
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1332
1353
  const iv = message.slice(ivStart, ctStart);
@@ -1337,11 +1358,20 @@ async function decryptBytes(keypair, message, opts) {
1337
1358
  opts?.info !== void 0 ? { info: opts.info } : void 0
1338
1359
  );
1339
1360
  const key = await importAesKeyDecryptOnly(keyBytes);
1340
- const pt = await subtle.decrypt(
1341
- { name: "AES-GCM", iv },
1342
- key,
1343
- ciphertext
1344
- );
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
+ }
1345
1375
  return new Uint8Array(pt);
1346
1376
  }
1347
1377
  __name(decryptBytes, "decryptBytes");
@@ -1362,7 +1392,8 @@ async function decryptFromString(keypair, message, opts) {
1362
1392
  __name(decryptFromString, "decryptFromString");
1363
1393
  function validateKeysize(keysize) {
1364
1394
  if (keysize !== 128 && keysize !== 256) {
1365
- throw new Error(
1395
+ throw new HpkeError(
1396
+ "ERR_INVALID_KEYSIZE",
1366
1397
  `invalid keysize: ${keysize} (expected 128 or 256)`
1367
1398
  );
1368
1399
  }
@@ -1370,7 +1401,8 @@ function validateKeysize(keysize) {
1370
1401
  __name(validateKeysize, "validateKeysize");
1371
1402
  function validateRawKeyBytes(raw) {
1372
1403
  if (raw.length !== 16 && raw.length !== 32) {
1373
- throw new Error(
1404
+ throw new HpkeError(
1405
+ "ERR_INVALID_AES_KEY",
1374
1406
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1375
1407
  );
1376
1408
  }
@@ -1384,19 +1416,20 @@ function normalizeInfo(info) {
1384
1416
  __name(normalizeInfo, "normalizeInfo");
1385
1417
  async function exportAesKeyBytes(key) {
1386
1418
  if (!key.extractable) {
1387
- throw new Error(
1419
+ throw new HpkeError(
1420
+ "ERR_INVALID_AES_KEY",
1388
1421
  "aesKey must be extractable: its raw bytes are what get sealed"
1389
1422
  );
1390
1423
  }
1391
1424
  return new Uint8Array(await subtle.exportKey("raw", key));
1392
1425
  }
1393
1426
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1394
- async function importAesKey(raw) {
1427
+ async function importAesKey(raw, extractable) {
1395
1428
  return subtle.importKey(
1396
1429
  "raw",
1397
1430
  raw,
1398
1431
  { name: "AES-GCM" },
1399
- true,
1432
+ extractable,
1400
1433
  ["encrypt", "decrypt"]
1401
1434
  );
1402
1435
  }
@@ -1451,7 +1484,10 @@ async function keySchedule(sharedSecret, info) {
1451
1484
  __name(keySchedule, "keySchedule");
1452
1485
  async function openRawBytes(keypair, wrapped, opts) {
1453
1486
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1454
- throw new Error("malformed envelope: too short");
1487
+ throw new HpkeError(
1488
+ "ERR_MALFORMED_ENVELOPE",
1489
+ "malformed envelope: too short"
1490
+ );
1455
1491
  }
1456
1492
  const info = normalizeInfo(opts?.info);
1457
1493
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1463,7 +1499,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1463
1499
  __name(openRawBytes, "openRawBytes");
1464
1500
  async function openBytes(keypair, wrapped, opts) {
1465
1501
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1466
- return importAesKey(keyBytes);
1502
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1467
1503
  }
1468
1504
  __name(openBytes, "openBytes");
1469
1505
  async function encryptKey(recipient, aesKey, opts) {
@@ -1487,8 +1523,11 @@ async function encryptKey(recipient, aesKey, opts) {
1487
1523
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1488
1524
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1489
1525
  const wrapped = concat2(enc, ciphertext);
1490
- const aesGcmKey = await importAesKey(keyBytes);
1491
- return { enc: wrapped, key: aesGcmKey };
1526
+ const aesGcmKey = await importAesKey(
1527
+ keyBytes,
1528
+ opts?.extractable ?? false
1529
+ );
1530
+ return { wrapped, key: aesGcmKey };
1492
1531
  }
1493
1532
  __name(encryptKey, "encryptKey");
1494
1533
 
@@ -1518,24 +1557,38 @@ async function aeadOpen(key, nonce, ciphertext) {
1518
1557
  false,
1519
1558
  ["decrypt"]
1520
1559
  );
1521
- const pt = await subtle2.decrypt(
1522
- { name: "AES-GCM", iv: nonce },
1523
- k,
1524
- ciphertext
1525
- );
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
+ }
1526
1574
  return new Uint8Array(pt);
1527
1575
  }
1528
1576
  __name(aeadOpen, "aeadOpen");
1529
1577
  async function encryptBytes(recipient, message, aesKey, opts) {
1530
1578
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1531
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1579
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1532
1580
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1533
1581
  const ct = new Uint8Array(await subtle2.encrypt(
1534
1582
  { name: "AES-GCM", iv },
1535
1583
  key,
1536
1584
  plaintext
1537
1585
  ));
1538
- 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
+ );
1539
1592
  }
1540
1593
  __name(encryptBytes, "encryptBytes");
1541
1594
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1601,7 +1654,8 @@ async function exportRawPublic(key) {
1601
1654
  __name(exportRawPublic, "exportRawPublic");
1602
1655
  async function importRawPublic(raw) {
1603
1656
  if (raw.length !== ENC_LENGTH) {
1604
- throw new Error(
1657
+ throw new HpkeError(
1658
+ "ERR_INVALID_RECIPIENT_KEY",
1605
1659
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1606
1660
  );
1607
1661
  }
@@ -1620,9 +1674,16 @@ async function resolveRecipientPublicKey(recipient) {
1620
1674
  }
1621
1675
  if (recipient instanceof CryptoKey) {
1622
1676
  if (recipient.type !== "public") {
1623
- 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
+ );
1624
1681
  }
1625
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1682
+ assertX25519Algorithm(
1683
+ recipient,
1684
+ "recipient CryptoKey",
1685
+ "ERR_INVALID_RECIPIENT_KEY"
1686
+ );
1626
1687
  return recipient;
1627
1688
  }
1628
1689
  if (typeof recipient.publicKey === "string") {
@@ -1632,17 +1693,28 @@ async function resolveRecipientPublicKey(recipient) {
1632
1693
  const pair = recipient;
1633
1694
  if (pair.publicKey instanceof CryptoKey) {
1634
1695
  if (pair.publicKey.type !== "public") {
1635
- 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
+ );
1636
1700
  }
1637
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1701
+ assertX25519Algorithm(
1702
+ pair.publicKey,
1703
+ "recipient keypair publicKey",
1704
+ "ERR_INVALID_RECIPIENT_KEY"
1705
+ );
1638
1706
  return pair.publicKey;
1639
1707
  }
1640
- throw new Error("unrecognized recipient key form");
1708
+ throw new HpkeError(
1709
+ "ERR_INVALID_RECIPIENT_KEY",
1710
+ "unrecognized recipient key form"
1711
+ );
1641
1712
  }
1642
1713
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1643
- function assertX25519Algorithm(key, context) {
1714
+ function assertX25519Algorithm(key, context, code2) {
1644
1715
  if (key.algorithm.name !== "X25519") {
1645
- throw new Error(
1716
+ throw new HpkeError(
1717
+ code2,
1646
1718
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1647
1719
  );
1648
1720
  }
@@ -1656,7 +1728,8 @@ async function dh(priv, pub) {
1656
1728
  );
1657
1729
  const bytes = new Uint8Array(bits);
1658
1730
  if (bytes.every((b) => b === 0)) {
1659
- throw new Error(
1731
+ throw new HpkeError(
1732
+ "ERR_SMALL_ORDER_KEY",
1660
1733
  "X25519 shared secret is all-zero (small-order public key?)"
1661
1734
  );
1662
1735
  }
@@ -1695,9 +1768,16 @@ async function encap(pkR) {
1695
1768
  __name(encap, "encap");
1696
1769
  async function decap(enc, keypair) {
1697
1770
  if (keypair.privateKey.type !== "private") {
1698
- 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
+ );
1699
1775
  }
1700
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1776
+ assertX25519Algorithm(
1777
+ keypair.privateKey,
1778
+ "keypair.privateKey",
1779
+ "ERR_INVALID_KEYPAIR"
1780
+ );
1701
1781
  const pkE = await subtle2.importKey(
1702
1782
  "raw",
1703
1783
  enc,
@@ -1711,6 +1791,35 @@ async function decap(enc, keypair) {
1711
1791
  return extractAndExpand(dhBytes, kemContext);
1712
1792
  }
1713
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");
1714
1823
  function i2osp(n, len) {
1715
1824
  const out = new Uint8Array(len);
1716
1825
  let v = n;
@@ -1743,6 +1852,7 @@ export {
1743
1852
  encryptToString,
1744
1853
  hmac,
1745
1854
  i2osp,
1855
+ keyId,
1746
1856
  labeledExpand,
1747
1857
  labeledExtract,
1748
1858
  resolveRecipientPublicKey