simple-hpke 0.2.0 → 0.3.3

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
@@ -32,7 +32,11 @@ export declare function aeadOpen(key: Uint8Array, nonce: Uint8Array, ciphertext:
32
32
  * @param message Plaintext to encrypt. A `string` is UTF-8 encoded.
33
33
  * @param aesKey Optional key, as either an AES-GCM `CryptoKey` or its raw
34
34
  * bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh key of
35
- * `opts.size` bits. A supplied `CryptoKey` MUST be extractable. Each
35
+ * `opts.size` bits. A supplied `CryptoKey` MUST be extractable and
36
+ * MUST have the `encrypt` usage (the message is encrypted under it
37
+ * directly; otherwise this throws `ERR_INVALID_AES_KEY`). It is used
38
+ * as-is and never re-imported, so the `opts.extractable` setting that
39
+ * `encryptKey` takes does not apply to it. Each
36
40
  * call picks a fresh random 96-bit IV for the message ciphertext, so
37
41
  * reusing the same `aesKey` across many calls carries the standard
38
42
  * birthday bound for random nonces (collision risk becomes
@@ -49,7 +53,7 @@ export declare function encryptBytes(recipient: RecipientKey, message: Uint8Arra
49
53
  info?: Uint8Array | string;
50
54
  }): Promise<Uint8Array>;
51
55
  /**
52
- * Like `encrypt`, but encodes the envelope bytes to a string — handy for
56
+ * Like `encrypt`, but encodes the envelope bytes to a string -- handy for
53
57
  * transports that carry text (JSON, URLs, headers). Decode with
54
58
  * `fromString(...)` (or any matching decoder) and pass the bytes to
55
59
  * `decrypt` / `decrypt.asString`. Exposed as `encrypt.asString`.
@@ -60,7 +64,11 @@ export declare function encryptBytes(recipient: RecipientKey, message: Uint8Arra
60
64
  * @param message Plaintext to encrypt. A `string` is UTF-8 encoded.
61
65
  * @param aesKey Optional key, as either an AES-GCM `CryptoKey` or its raw
62
66
  * bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh key of
63
- * `opts.size` bits. A supplied `CryptoKey` MUST be extractable. Each
67
+ * `opts.size` bits. A supplied `CryptoKey` MUST be extractable and
68
+ * MUST have the `encrypt` usage (the message is encrypted under it
69
+ * directly; otherwise this throws `ERR_INVALID_AES_KEY`). It is used
70
+ * as-is and never re-imported, so the `opts.extractable` setting that
71
+ * `encryptKey` takes does not apply to it. Each
64
72
  * call picks a fresh random 96-bit IV for the message ciphertext, so
65
73
  * reusing the same `aesKey` across many calls carries the standard
66
74
  * birthday bound for random nonces (collision risk becomes
@@ -85,6 +93,51 @@ export declare function encap(pkR: CryptoKey): Promise<{
85
93
  enc: Uint8Array;
86
94
  }>;
87
95
  export declare function decap(enc: Uint8Array, keypair: CryptoKeyPair): Promise<Uint8Array>;
96
+ /**
97
+ * A stable fingerprint for an AES-GCM key: the same key bytes always give
98
+ * the same string, and two different keys effectively never collide.
99
+ *
100
+ * Derived from the key's *behavior* rather than its bytes, so it works on
101
+ * a non-extractable key -- including one rehydrated from IndexedDB, and
102
+ * the keys `create` / `encryptKey` / `open` return by default. The obvious
103
+ * implementation, `exportKey('raw')` then SHA-256, would force every such
104
+ * key to be extractable. HKDF is not an option either: WebCrypto cannot
105
+ * convert an AES-GCM key into an HKDF key, and `deriveBits` only accepts a
106
+ * key imported under HKDF, which needs the raw bytes a non-extractable key
107
+ * withholds. AES-GCM as a PRF is the only primitive such a key still
108
+ * exposes.
109
+ *
110
+ * The derivation is fixed forever (see `src/constants.ts`):
111
+ *
112
+ * ```
113
+ * LABEL = utf8("simple-hpke/keyId/v1")
114
+ * N = SHA-256(LABEL)[0..12] // fixed nonce
115
+ * C = AES-GCM(key, N, LABEL) // ciphertext || 16-byte tag
116
+ * keyId = base64url(SHA-256(LABEL || C))
117
+ * ```
118
+ *
119
+ * @param key An AES-GCM `CryptoKey` with the `encrypt` usage. It does not
120
+ * need to be extractable.
121
+ * @returns The fingerprint: the full 32-byte digest, base64url, unpadded
122
+ * (43 characters).
123
+ * @throws {HpkeError} `ERR_INVALID_AES_KEY` if `key` is not an AES-GCM key
124
+ * or cannot encrypt.
125
+ */
126
+ export declare function keyId(key: CryptoKey): Promise<string>;
127
+ /**
128
+ * Check whether `key` matches a stored `keyId`.
129
+ *
130
+ * Compares in constant time over the string. Errors from `keyId`
131
+ * propagate: a key that cannot be fingerprinted rejects rather than
132
+ * resolving `false`, so "cannot check" never reads as "wrong key".
133
+ *
134
+ * @param key An AES-GCM `CryptoKey` with the `encrypt` usage.
135
+ * @param expected A `keyId` string recorded earlier.
136
+ * @returns `true` if `keyId(key)` equals `expected`, else `false`.
137
+ * @throws {HpkeError} `ERR_INVALID_AES_KEY` if `key` is not an AES-GCM key
138
+ * or cannot encrypt.
139
+ */
140
+ export declare function verifyKeyId(key: CryptoKey, expected: string): Promise<boolean>;
88
141
  export declare function i2osp(n: number, len: number): Uint8Array;
89
142
  export declare function concat(...arrays: Uint8Array[]): Uint8Array;
90
143
  //# 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":"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"}
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");
@@ -1351,7 +1381,16 @@ async function decryptToString(keypair, message, opts) {
1351
1381
  }
1352
1382
  __name(decryptToString, "decryptToString");
1353
1383
  async function decryptFromString(keypair, message, opts) {
1354
- const bytes = fromString2(message, "base64url");
1384
+ let bytes;
1385
+ try {
1386
+ bytes = fromString2(message, "base64url");
1387
+ } catch (err) {
1388
+ throw new HpkeError(
1389
+ "ERR_MALFORMED_MESSAGE",
1390
+ "malformed message: not base64url",
1391
+ { cause: err }
1392
+ );
1393
+ }
1355
1394
  const plaintext = await decryptBytes(
1356
1395
  keypair,
1357
1396
  bytes,
@@ -1362,7 +1401,8 @@ async function decryptFromString(keypair, message, opts) {
1362
1401
  __name(decryptFromString, "decryptFromString");
1363
1402
  function validateKeysize(keysize) {
1364
1403
  if (keysize !== 128 && keysize !== 256) {
1365
- throw new Error(
1404
+ throw new HpkeError(
1405
+ "ERR_INVALID_KEYSIZE",
1366
1406
  `invalid keysize: ${keysize} (expected 128 or 256)`
1367
1407
  );
1368
1408
  }
@@ -1370,7 +1410,8 @@ function validateKeysize(keysize) {
1370
1410
  __name(validateKeysize, "validateKeysize");
1371
1411
  function validateRawKeyBytes(raw) {
1372
1412
  if (raw.length !== 16 && raw.length !== 32) {
1373
- throw new Error(
1413
+ throw new HpkeError(
1414
+ "ERR_INVALID_AES_KEY",
1374
1415
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1375
1416
  );
1376
1417
  }
@@ -1384,19 +1425,20 @@ function normalizeInfo(info) {
1384
1425
  __name(normalizeInfo, "normalizeInfo");
1385
1426
  async function exportAesKeyBytes(key) {
1386
1427
  if (!key.extractable) {
1387
- throw new Error(
1428
+ throw new HpkeError(
1429
+ "ERR_INVALID_AES_KEY",
1388
1430
  "aesKey must be extractable: its raw bytes are what get sealed"
1389
1431
  );
1390
1432
  }
1391
1433
  return new Uint8Array(await subtle.exportKey("raw", key));
1392
1434
  }
1393
1435
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1394
- async function importAesKey(raw) {
1436
+ async function importAesKey(raw, extractable) {
1395
1437
  return subtle.importKey(
1396
1438
  "raw",
1397
1439
  raw,
1398
1440
  { name: "AES-GCM" },
1399
- true,
1441
+ extractable,
1400
1442
  ["encrypt", "decrypt"]
1401
1443
  );
1402
1444
  }
@@ -1451,7 +1493,10 @@ async function keySchedule(sharedSecret, info) {
1451
1493
  __name(keySchedule, "keySchedule");
1452
1494
  async function openRawBytes(keypair, wrapped, opts) {
1453
1495
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1454
- throw new Error("malformed envelope: too short");
1496
+ throw new HpkeError(
1497
+ "ERR_MALFORMED_ENVELOPE",
1498
+ "malformed envelope: too short"
1499
+ );
1455
1500
  }
1456
1501
  const info = normalizeInfo(opts?.info);
1457
1502
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1463,7 +1508,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1463
1508
  __name(openRawBytes, "openRawBytes");
1464
1509
  async function openBytes(keypair, wrapped, opts) {
1465
1510
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1466
- return importAesKey(keyBytes);
1511
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1467
1512
  }
1468
1513
  __name(openBytes, "openBytes");
1469
1514
  async function encryptKey(recipient, aesKey, opts) {
@@ -1473,6 +1518,12 @@ async function encryptKey(recipient, aesKey, opts) {
1473
1518
  validateRawKeyBytes(aesKey);
1474
1519
  keyBytes = aesKey;
1475
1520
  } else if (aesKey) {
1521
+ if (aesKey.algorithm.name !== "AES-GCM") {
1522
+ throw new HpkeError(
1523
+ "ERR_INVALID_AES_KEY",
1524
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1525
+ );
1526
+ }
1476
1527
  keyBytes = await exportAesKeyBytes(aesKey);
1477
1528
  validateRawKeyBytes(keyBytes);
1478
1529
  } else {
@@ -1487,8 +1538,12 @@ async function encryptKey(recipient, aesKey, opts) {
1487
1538
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1488
1539
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1489
1540
  const wrapped = concat2(enc, ciphertext);
1490
- const aesGcmKey = await importAesKey(keyBytes);
1491
- return { enc: wrapped, key: aesGcmKey };
1541
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1542
+ const aesGcmKey = await importAesKey(
1543
+ keyBytes,
1544
+ opts?.extractable ?? false
1545
+ );
1546
+ return { wrapped, key: aesGcmKey };
1492
1547
  }
1493
1548
  __name(encryptKey, "encryptKey");
1494
1549
 
@@ -1518,24 +1573,44 @@ async function aeadOpen(key, nonce, ciphertext) {
1518
1573
  false,
1519
1574
  ["decrypt"]
1520
1575
  );
1521
- const pt = await subtle2.decrypt(
1522
- { name: "AES-GCM", iv: nonce },
1523
- k,
1524
- ciphertext
1525
- );
1576
+ let pt;
1577
+ try {
1578
+ pt = await subtle2.decrypt(
1579
+ { name: "AES-GCM", iv: nonce },
1580
+ k,
1581
+ ciphertext
1582
+ );
1583
+ } catch (err) {
1584
+ throw new HpkeError(
1585
+ "ERR_DECRYPT_FAILED",
1586
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1587
+ { cause: err }
1588
+ );
1589
+ }
1526
1590
  return new Uint8Array(pt);
1527
1591
  }
1528
1592
  __name(aeadOpen, "aeadOpen");
1529
1593
  async function encryptBytes(recipient, message, aesKey, opts) {
1594
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1595
+ throw new HpkeError(
1596
+ "ERR_INVALID_AES_KEY",
1597
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1598
+ );
1599
+ }
1530
1600
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1531
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1601
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1532
1602
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1533
1603
  const ct = new Uint8Array(await subtle2.encrypt(
1534
1604
  { name: "AES-GCM", iv },
1535
1605
  key,
1536
1606
  plaintext
1537
1607
  ));
1538
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1608
+ return concat2(
1609
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1610
+ wrapped,
1611
+ iv,
1612
+ ct
1613
+ );
1539
1614
  }
1540
1615
  __name(encryptBytes, "encryptBytes");
1541
1616
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1601,7 +1676,8 @@ async function exportRawPublic(key) {
1601
1676
  __name(exportRawPublic, "exportRawPublic");
1602
1677
  async function importRawPublic(raw) {
1603
1678
  if (raw.length !== ENC_LENGTH) {
1604
- throw new Error(
1679
+ throw new HpkeError(
1680
+ "ERR_INVALID_RECIPIENT_KEY",
1605
1681
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1606
1682
  );
1607
1683
  }
@@ -1619,44 +1695,83 @@ async function resolveRecipientPublicKey(recipient) {
1619
1695
  return importRawPublic(recipient);
1620
1696
  }
1621
1697
  if (recipient instanceof CryptoKey) {
1622
- if (recipient.type !== "public") {
1623
- throw new Error("recipient CryptoKey must be a public key");
1624
- }
1625
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1626
- return recipient;
1698
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1627
1699
  }
1628
1700
  if (typeof recipient.publicKey === "string") {
1629
1701
  const { publicKey, encoding } = recipient;
1630
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1702
+ let decoded;
1703
+ try {
1704
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1705
+ } catch (err) {
1706
+ throw new HpkeError(
1707
+ "ERR_INVALID_RECIPIENT_KEY",
1708
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1709
+ { cause: err }
1710
+ );
1711
+ }
1712
+ return importRawPublic(decoded);
1631
1713
  }
1632
1714
  const pair = recipient;
1633
1715
  if (pair.publicKey instanceof CryptoKey) {
1634
- if (pair.publicKey.type !== "public") {
1635
- throw new Error("recipient keypair publicKey must be a public key");
1636
- }
1637
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1638
- return pair.publicKey;
1716
+ return assertRecipientPublicKey(
1717
+ pair.publicKey,
1718
+ "recipient keypair publicKey"
1719
+ );
1639
1720
  }
1640
- throw new Error("unrecognized recipient key form");
1721
+ throw new HpkeError(
1722
+ "ERR_INVALID_RECIPIENT_KEY",
1723
+ "unrecognized recipient key form"
1724
+ );
1641
1725
  }
1642
1726
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1643
- function assertX25519Algorithm(key, context) {
1727
+ function assertRecipientPublicKey(key, context) {
1728
+ if (key.type !== "public") {
1729
+ throw new HpkeError(
1730
+ "ERR_INVALID_RECIPIENT_KEY",
1731
+ `${context} must be a public key`
1732
+ );
1733
+ }
1734
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1735
+ if (!key.extractable) {
1736
+ throw new HpkeError(
1737
+ "ERR_INVALID_RECIPIENT_KEY",
1738
+ `${context} must be extractable (the KEM exports it)`
1739
+ );
1740
+ }
1741
+ return key;
1742
+ }
1743
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1744
+ function assertX25519Algorithm(key, context, code2) {
1644
1745
  if (key.algorithm.name !== "X25519") {
1645
- throw new Error(
1746
+ throw new HpkeError(
1747
+ code2,
1646
1748
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1647
1749
  );
1648
1750
  }
1649
1751
  }
1650
1752
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1651
1753
  async function dh(priv, pub) {
1652
- const bits = await subtle2.deriveBits(
1653
- { name: "X25519", public: pub },
1654
- priv,
1655
- 256
1656
- );
1754
+ let bits;
1755
+ try {
1756
+ bits = await subtle2.deriveBits(
1757
+ { name: "X25519", public: pub },
1758
+ priv,
1759
+ 256
1760
+ );
1761
+ } catch (err) {
1762
+ if (err?.name === "OperationError") {
1763
+ throw new HpkeError(
1764
+ "ERR_SMALL_ORDER_KEY",
1765
+ "X25519 deriveBits rejected a small-order public key",
1766
+ { cause: err }
1767
+ );
1768
+ }
1769
+ throw err;
1770
+ }
1657
1771
  const bytes = new Uint8Array(bits);
1658
1772
  if (bytes.every((b) => b === 0)) {
1659
- throw new Error(
1773
+ throw new HpkeError(
1774
+ "ERR_SMALL_ORDER_KEY",
1660
1775
  "X25519 shared secret is all-zero (small-order public key?)"
1661
1776
  );
1662
1777
  }
@@ -1695,9 +1810,33 @@ async function encap(pkR) {
1695
1810
  __name(encap, "encap");
1696
1811
  async function decap(enc, keypair) {
1697
1812
  if (keypair.privateKey.type !== "private") {
1698
- throw new Error("keypair.privateKey must be a private key");
1813
+ throw new HpkeError(
1814
+ "ERR_INVALID_KEYPAIR",
1815
+ "keypair.privateKey must be a private key"
1816
+ );
1817
+ }
1818
+ assertX25519Algorithm(
1819
+ keypair.privateKey,
1820
+ "keypair.privateKey",
1821
+ "ERR_INVALID_KEYPAIR"
1822
+ );
1823
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1824
+ throw new HpkeError(
1825
+ "ERR_INVALID_KEYPAIR",
1826
+ "keypair.privateKey must have the deriveBits usage"
1827
+ );
1828
+ }
1829
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1830
+ throw new HpkeError(
1831
+ "ERR_INVALID_KEYPAIR",
1832
+ "keypair.publicKey must be an extractable public key"
1833
+ );
1699
1834
  }
1700
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1835
+ assertX25519Algorithm(
1836
+ keypair.publicKey,
1837
+ "keypair.publicKey",
1838
+ "ERR_INVALID_KEYPAIR"
1839
+ );
1701
1840
  const pkE = await subtle2.importKey(
1702
1841
  "raw",
1703
1842
  enc,
@@ -1711,6 +1850,45 @@ async function decap(enc, keypair) {
1711
1850
  return extractAndExpand(dhBytes, kemContext);
1712
1851
  }
1713
1852
  __name(decap, "decap");
1853
+ async function keyId(key) {
1854
+ if (key.algorithm.name !== "AES-GCM") {
1855
+ throw new HpkeError(
1856
+ "ERR_INVALID_AES_KEY",
1857
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1858
+ );
1859
+ }
1860
+ if (!key.usages.includes("encrypt")) {
1861
+ throw new HpkeError(
1862
+ "ERR_INVALID_AES_KEY",
1863
+ "keyId needs a key with the 'encrypt' usage"
1864
+ );
1865
+ }
1866
+ const labelHash = new Uint8Array(
1867
+ await subtle2.digest("SHA-256", KEY_ID_LABEL)
1868
+ );
1869
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1870
+ const ciphertext = new Uint8Array(await subtle2.encrypt(
1871
+ { name: "AES-GCM", iv: nonce },
1872
+ key,
1873
+ KEY_ID_LABEL
1874
+ ));
1875
+ const digest2 = await subtle2.digest(
1876
+ "SHA-256",
1877
+ concat2(KEY_ID_LABEL, ciphertext)
1878
+ );
1879
+ return toString2(new Uint8Array(digest2), "base64url");
1880
+ }
1881
+ __name(keyId, "keyId");
1882
+ async function verifyKeyId(key, expected) {
1883
+ const actual = await keyId(key);
1884
+ if (actual.length !== expected.length) return false;
1885
+ let diff = 0;
1886
+ for (let i = 0; i < actual.length; i++) {
1887
+ diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
1888
+ }
1889
+ return diff === 0;
1890
+ }
1891
+ __name(verifyKeyId, "verifyKeyId");
1714
1892
  function i2osp(n, len) {
1715
1893
  const out = new Uint8Array(len);
1716
1894
  let v = n;
@@ -1743,8 +1921,10 @@ export {
1743
1921
  encryptToString,
1744
1922
  hmac,
1745
1923
  i2osp,
1924
+ keyId,
1746
1925
  labeledExpand,
1747
1926
  labeledExtract,
1748
- resolveRecipientPublicKey
1927
+ resolveRecipientPublicKey,
1928
+ verifyKeyId
1749
1929
  };
1750
1930
  //# sourceMappingURL=util.js.map