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/meta.json CHANGED
@@ -690,8 +690,19 @@
690
690
  ],
691
691
  "format": "esm"
692
692
  },
693
+ "src/errors.ts": {
694
+ "bytes": 975,
695
+ "imports": [
696
+ {
697
+ "path": "<runtime>",
698
+ "kind": "import-statement",
699
+ "external": true
700
+ }
701
+ ],
702
+ "format": "esm"
703
+ },
693
704
  "src/index.ts": {
694
- "bytes": 12639,
705
+ "bytes": 15697,
695
706
  "imports": [
696
707
  {
697
708
  "path": "node_modules/uint8arrays/dist/src/index.js",
@@ -703,11 +714,21 @@
703
714
  "kind": "import-statement",
704
715
  "original": "./util"
705
716
  },
717
+ {
718
+ "path": "src/errors.ts",
719
+ "kind": "import-statement",
720
+ "original": "./errors"
721
+ },
706
722
  {
707
723
  "path": "src/constants.ts",
708
724
  "kind": "import-statement",
709
725
  "original": "./constants"
710
726
  },
727
+ {
728
+ "path": "src/errors.ts",
729
+ "kind": "import-statement",
730
+ "original": "./errors"
731
+ },
711
732
  {
712
733
  "path": "<runtime>",
713
734
  "kind": "import-statement",
@@ -717,7 +738,7 @@
717
738
  "format": "esm"
718
739
  },
719
740
  "src/util.ts": {
720
- "bytes": 13967,
741
+ "bytes": 22056,
721
742
  "imports": [
722
743
  {
723
744
  "path": "node_modules/uint8arrays/dist/src/index.js",
@@ -729,6 +750,11 @@
729
750
  "kind": "import-statement",
730
751
  "original": "./index.js"
731
752
  },
753
+ {
754
+ "path": "src/errors.ts",
755
+ "kind": "import-statement",
756
+ "original": "./errors.js"
757
+ },
732
758
  {
733
759
  "path": "src/constants.ts",
734
760
  "kind": "import-statement",
@@ -743,7 +769,7 @@
743
769
  "format": "esm"
744
770
  },
745
771
  "src/constants.ts": {
746
- "bytes": 1348,
772
+ "bytes": 2125,
747
773
  "imports": [
748
774
  {
749
775
  "path": "src/util.ts",
@@ -759,7 +785,7 @@
759
785
  "imports": [],
760
786
  "exports": [],
761
787
  "inputs": {},
762
- "bytes": 112940
788
+ "bytes": 128024
763
789
  },
764
790
  "dist/constants.js": {
765
791
  "imports": [],
@@ -772,6 +798,8 @@
772
798
  "KDF_ID",
773
799
  "KEM_ID",
774
800
  "KEM_SUITE_ID",
801
+ "KEY_ID_LABEL",
802
+ "KEY_ID_NONCE_LENGTH",
775
803
  "MODE_BASE",
776
804
  "NK",
777
805
  "NN",
@@ -864,32 +892,57 @@
864
892
  "node_modules/uint8arrays/dist/src/to-string.js": {
865
893
  "bytesInOutput": 258
866
894
  },
895
+ "src/errors.ts": {
896
+ "bytesInOutput": 215
897
+ },
867
898
  "src/index.ts": {
868
- "bytesInOutput": 5486
899
+ "bytesInOutput": 6418
869
900
  },
870
901
  "src/util.ts": {
871
- "bytesInOutput": 6618
902
+ "bytesInOutput": 8581
872
903
  },
873
904
  "src/constants.ts": {
874
- "bytesInOutput": 452
905
+ "bytesInOutput": 551
906
+ }
907
+ },
908
+ "bytes": 54253
909
+ },
910
+ "dist/errors.js.map": {
911
+ "imports": [],
912
+ "exports": [],
913
+ "inputs": {},
914
+ "bytes": 1293
915
+ },
916
+ "dist/errors.js": {
917
+ "imports": [],
918
+ "exports": [
919
+ "HpkeError"
920
+ ],
921
+ "entryPoint": "src/errors.ts",
922
+ "inputs": {
923
+ "src/errors.ts": {
924
+ "bytesInOutput": 213
875
925
  }
876
926
  },
877
- "bytes": 50987
927
+ "bytes": 419
878
928
  },
879
929
  "dist/index.js.map": {
880
930
  "imports": [],
881
931
  "exports": [],
882
932
  "inputs": {},
883
- "bytes": 113074
933
+ "bytes": 128969
884
934
  },
885
935
  "dist/index.js": {
886
936
  "imports": [],
887
937
  "exports": [
938
+ "HpkeError",
888
939
  "create",
889
940
  "decrypt",
890
941
  "encrypt",
891
942
  "encryptKey",
892
- "open"
943
+ "keyId",
944
+ "open",
945
+ "verifyKeyId"
893
946
  ],
894
947
  "entryPoint": "src/index.ts",
895
948
  "inputs": {
@@ -977,23 +1030,26 @@
977
1030
  "node_modules/uint8arrays/dist/src/to-string.js": {
978
1031
  "bytesInOutput": 258
979
1032
  },
1033
+ "src/errors.ts": {
1034
+ "bytesInOutput": 215
1035
+ },
980
1036
  "src/constants.ts": {
981
- "bytesInOutput": 452
1037
+ "bytesInOutput": 551
982
1038
  },
983
1039
  "src/util.ts": {
984
- "bytesInOutput": 6605
1040
+ "bytesInOutput": 9693
985
1041
  },
986
1042
  "src/index.ts": {
987
- "bytesInOutput": 5649
1043
+ "bytesInOutput": 6589
988
1044
  }
989
1045
  },
990
- "bytes": 51032
1046
+ "bytes": 55429
991
1047
  },
992
1048
  "dist/util.js.map": {
993
1049
  "imports": [],
994
1050
  "exports": [],
995
1051
  "inputs": {},
996
- "bytes": 112937
1052
+ "bytes": 128835
997
1053
  },
998
1054
  "dist/util.js": {
999
1055
  "imports": [],
@@ -1007,9 +1063,11 @@
1007
1063
  "encryptToString",
1008
1064
  "hmac",
1009
1065
  "i2osp",
1066
+ "keyId",
1010
1067
  "labeledExpand",
1011
1068
  "labeledExtract",
1012
- "resolveRecipientPublicKey"
1069
+ "resolveRecipientPublicKey",
1070
+ "verifyKeyId"
1013
1071
  ],
1014
1072
  "entryPoint": "src/util.ts",
1015
1073
  "inputs": {
@@ -1097,17 +1155,20 @@
1097
1155
  "node_modules/uint8arrays/dist/src/to-string.js": {
1098
1156
  "bytesInOutput": 258
1099
1157
  },
1158
+ "src/errors.ts": {
1159
+ "bytesInOutput": 215
1160
+ },
1100
1161
  "src/constants.ts": {
1101
- "bytesInOutput": 452
1162
+ "bytesInOutput": 551
1102
1163
  },
1103
1164
  "src/index.ts": {
1104
- "bytesInOutput": 5486
1165
+ "bytesInOutput": 6418
1105
1166
  },
1106
1167
  "src/util.ts": {
1107
- "bytesInOutput": 6618
1168
+ "bytesInOutput": 9709
1108
1169
  }
1109
1170
  },
1110
- "bytes": 50995
1171
+ "bytes": 55374
1111
1172
  }
1112
1173
  }
1113
1174
  }
package/dist/util.cjs CHANGED
@@ -30,9 +30,11 @@ __export(util_exports, {
30
30
  encryptToString: () => encryptToString,
31
31
  hmac: () => hmac,
32
32
  i2osp: () => i2osp,
33
+ keyId: () => keyId,
33
34
  labeledExpand: () => labeledExpand,
34
35
  labeledExtract: () => labeledExtract,
35
- resolveRecipientPublicKey: () => resolveRecipientPublicKey
36
+ resolveRecipientPublicKey: () => resolveRecipientPublicKey,
37
+ verifyKeyId: () => verifyKeyId
36
38
  });
37
39
  module.exports = __toCommonJS(util_exports);
38
40
 
@@ -1314,6 +1316,19 @@ function toString2(array, encoding = "utf8") {
1314
1316
  }
1315
1317
  __name(toString2, "toString");
1316
1318
 
1319
+ // src/errors.ts
1320
+ var HpkeError = class extends Error {
1321
+ static {
1322
+ __name(this, "HpkeError");
1323
+ }
1324
+ code;
1325
+ constructor(code2, message, opts) {
1326
+ super(message, opts);
1327
+ this.name = "HpkeError";
1328
+ this.code = code2;
1329
+ }
1330
+ };
1331
+
1317
1332
  // src/constants.ts
1318
1333
  var KEM_ID = 32;
1319
1334
  var KDF_ID = 1;
@@ -1336,6 +1351,8 @@ var HPKE_SUITE_ID = concat2(
1336
1351
  i2osp(KDF_ID, 2),
1337
1352
  i2osp(AEAD_ID, 2)
1338
1353
  );
1354
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1355
+ var KEY_ID_NONCE_LENGTH = 12;
1339
1356
 
1340
1357
  // src/index.ts
1341
1358
  var subtle = globalThis.crypto.subtle;
@@ -1351,13 +1368,19 @@ var decrypt = Object.assign(decryptBytes, {
1351
1368
  });
1352
1369
  async function decryptBytes(keypair, message, opts) {
1353
1370
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1354
- throw new Error("malformed message: too short");
1371
+ throw new HpkeError(
1372
+ "ERR_MALFORMED_MESSAGE",
1373
+ "malformed message: too short"
1374
+ );
1355
1375
  }
1356
1376
  const wrappedLen = message[0] << 8 | message[1];
1357
1377
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1358
1378
  const ctStart = ivStart + NN;
1359
1379
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1360
- throw new Error("malformed message: too short");
1380
+ throw new HpkeError(
1381
+ "ERR_MALFORMED_MESSAGE",
1382
+ "malformed message: too short"
1383
+ );
1361
1384
  }
1362
1385
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1363
1386
  const iv = message.slice(ivStart, ctStart);
@@ -1368,11 +1391,20 @@ async function decryptBytes(keypair, message, opts) {
1368
1391
  opts?.info !== void 0 ? { info: opts.info } : void 0
1369
1392
  );
1370
1393
  const key = await importAesKeyDecryptOnly(keyBytes);
1371
- const pt = await subtle.decrypt(
1372
- { name: "AES-GCM", iv },
1373
- key,
1374
- ciphertext
1375
- );
1394
+ let pt;
1395
+ try {
1396
+ pt = await subtle.decrypt(
1397
+ { name: "AES-GCM", iv },
1398
+ key,
1399
+ ciphertext
1400
+ );
1401
+ } catch (err) {
1402
+ throw new HpkeError(
1403
+ "ERR_DECRYPT_FAILED",
1404
+ "failed to decrypt the message: tampered ciphertext",
1405
+ { cause: err }
1406
+ );
1407
+ }
1376
1408
  return new Uint8Array(pt);
1377
1409
  }
1378
1410
  __name(decryptBytes, "decryptBytes");
@@ -1382,7 +1414,16 @@ async function decryptToString(keypair, message, opts) {
1382
1414
  }
1383
1415
  __name(decryptToString, "decryptToString");
1384
1416
  async function decryptFromString(keypair, message, opts) {
1385
- const bytes = fromString2(message, "base64url");
1417
+ let bytes;
1418
+ try {
1419
+ bytes = fromString2(message, "base64url");
1420
+ } catch (err) {
1421
+ throw new HpkeError(
1422
+ "ERR_MALFORMED_MESSAGE",
1423
+ "malformed message: not base64url",
1424
+ { cause: err }
1425
+ );
1426
+ }
1386
1427
  const plaintext = await decryptBytes(
1387
1428
  keypair,
1388
1429
  bytes,
@@ -1393,7 +1434,8 @@ async function decryptFromString(keypair, message, opts) {
1393
1434
  __name(decryptFromString, "decryptFromString");
1394
1435
  function validateKeysize(keysize) {
1395
1436
  if (keysize !== 128 && keysize !== 256) {
1396
- throw new Error(
1437
+ throw new HpkeError(
1438
+ "ERR_INVALID_KEYSIZE",
1397
1439
  `invalid keysize: ${keysize} (expected 128 or 256)`
1398
1440
  );
1399
1441
  }
@@ -1401,7 +1443,8 @@ function validateKeysize(keysize) {
1401
1443
  __name(validateKeysize, "validateKeysize");
1402
1444
  function validateRawKeyBytes(raw) {
1403
1445
  if (raw.length !== 16 && raw.length !== 32) {
1404
- throw new Error(
1446
+ throw new HpkeError(
1447
+ "ERR_INVALID_AES_KEY",
1405
1448
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1406
1449
  );
1407
1450
  }
@@ -1415,19 +1458,20 @@ function normalizeInfo(info) {
1415
1458
  __name(normalizeInfo, "normalizeInfo");
1416
1459
  async function exportAesKeyBytes(key) {
1417
1460
  if (!key.extractable) {
1418
- throw new Error(
1461
+ throw new HpkeError(
1462
+ "ERR_INVALID_AES_KEY",
1419
1463
  "aesKey must be extractable: its raw bytes are what get sealed"
1420
1464
  );
1421
1465
  }
1422
1466
  return new Uint8Array(await subtle.exportKey("raw", key));
1423
1467
  }
1424
1468
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1425
- async function importAesKey(raw) {
1469
+ async function importAesKey(raw, extractable) {
1426
1470
  return subtle.importKey(
1427
1471
  "raw",
1428
1472
  raw,
1429
1473
  { name: "AES-GCM" },
1430
- true,
1474
+ extractable,
1431
1475
  ["encrypt", "decrypt"]
1432
1476
  );
1433
1477
  }
@@ -1482,7 +1526,10 @@ async function keySchedule(sharedSecret, info) {
1482
1526
  __name(keySchedule, "keySchedule");
1483
1527
  async function openRawBytes(keypair, wrapped, opts) {
1484
1528
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1485
- throw new Error("malformed envelope: too short");
1529
+ throw new HpkeError(
1530
+ "ERR_MALFORMED_ENVELOPE",
1531
+ "malformed envelope: too short"
1532
+ );
1486
1533
  }
1487
1534
  const info = normalizeInfo(opts?.info);
1488
1535
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1494,7 +1541,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1494
1541
  __name(openRawBytes, "openRawBytes");
1495
1542
  async function openBytes(keypair, wrapped, opts) {
1496
1543
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1497
- return importAesKey(keyBytes);
1544
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1498
1545
  }
1499
1546
  __name(openBytes, "openBytes");
1500
1547
  async function encryptKey(recipient, aesKey, opts) {
@@ -1504,6 +1551,12 @@ async function encryptKey(recipient, aesKey, opts) {
1504
1551
  validateRawKeyBytes(aesKey);
1505
1552
  keyBytes = aesKey;
1506
1553
  } else if (aesKey) {
1554
+ if (aesKey.algorithm.name !== "AES-GCM") {
1555
+ throw new HpkeError(
1556
+ "ERR_INVALID_AES_KEY",
1557
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1558
+ );
1559
+ }
1507
1560
  keyBytes = await exportAesKeyBytes(aesKey);
1508
1561
  validateRawKeyBytes(keyBytes);
1509
1562
  } else {
@@ -1518,8 +1571,12 @@ async function encryptKey(recipient, aesKey, opts) {
1518
1571
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1519
1572
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1520
1573
  const wrapped = concat2(enc, ciphertext);
1521
- const aesGcmKey = await importAesKey(keyBytes);
1522
- return { enc: wrapped, key: aesGcmKey };
1574
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1575
+ const aesGcmKey = await importAesKey(
1576
+ keyBytes,
1577
+ opts?.extractable ?? false
1578
+ );
1579
+ return { wrapped, key: aesGcmKey };
1523
1580
  }
1524
1581
  __name(encryptKey, "encryptKey");
1525
1582
 
@@ -1549,24 +1606,44 @@ async function aeadOpen(key, nonce, ciphertext) {
1549
1606
  false,
1550
1607
  ["decrypt"]
1551
1608
  );
1552
- const pt = await subtle2.decrypt(
1553
- { name: "AES-GCM", iv: nonce },
1554
- k,
1555
- ciphertext
1556
- );
1609
+ let pt;
1610
+ try {
1611
+ pt = await subtle2.decrypt(
1612
+ { name: "AES-GCM", iv: nonce },
1613
+ k,
1614
+ ciphertext
1615
+ );
1616
+ } catch (err) {
1617
+ throw new HpkeError(
1618
+ "ERR_DECRYPT_FAILED",
1619
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1620
+ { cause: err }
1621
+ );
1622
+ }
1557
1623
  return new Uint8Array(pt);
1558
1624
  }
1559
1625
  __name(aeadOpen, "aeadOpen");
1560
1626
  async function encryptBytes(recipient, message, aesKey, opts) {
1627
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1628
+ throw new HpkeError(
1629
+ "ERR_INVALID_AES_KEY",
1630
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1631
+ );
1632
+ }
1561
1633
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1562
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1634
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1563
1635
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1564
1636
  const ct = new Uint8Array(await subtle2.encrypt(
1565
1637
  { name: "AES-GCM", iv },
1566
1638
  key,
1567
1639
  plaintext
1568
1640
  ));
1569
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1641
+ return concat2(
1642
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1643
+ wrapped,
1644
+ iv,
1645
+ ct
1646
+ );
1570
1647
  }
1571
1648
  __name(encryptBytes, "encryptBytes");
1572
1649
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1632,7 +1709,8 @@ async function exportRawPublic(key) {
1632
1709
  __name(exportRawPublic, "exportRawPublic");
1633
1710
  async function importRawPublic(raw) {
1634
1711
  if (raw.length !== ENC_LENGTH) {
1635
- throw new Error(
1712
+ throw new HpkeError(
1713
+ "ERR_INVALID_RECIPIENT_KEY",
1636
1714
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1637
1715
  );
1638
1716
  }
@@ -1650,44 +1728,83 @@ async function resolveRecipientPublicKey(recipient) {
1650
1728
  return importRawPublic(recipient);
1651
1729
  }
1652
1730
  if (recipient instanceof CryptoKey) {
1653
- if (recipient.type !== "public") {
1654
- throw new Error("recipient CryptoKey must be a public key");
1655
- }
1656
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1657
- return recipient;
1731
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1658
1732
  }
1659
1733
  if (typeof recipient.publicKey === "string") {
1660
1734
  const { publicKey, encoding } = recipient;
1661
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1735
+ let decoded;
1736
+ try {
1737
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1738
+ } catch (err) {
1739
+ throw new HpkeError(
1740
+ "ERR_INVALID_RECIPIENT_KEY",
1741
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1742
+ { cause: err }
1743
+ );
1744
+ }
1745
+ return importRawPublic(decoded);
1662
1746
  }
1663
1747
  const pair = recipient;
1664
1748
  if (pair.publicKey instanceof CryptoKey) {
1665
- if (pair.publicKey.type !== "public") {
1666
- throw new Error("recipient keypair publicKey must be a public key");
1667
- }
1668
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1669
- return pair.publicKey;
1749
+ return assertRecipientPublicKey(
1750
+ pair.publicKey,
1751
+ "recipient keypair publicKey"
1752
+ );
1670
1753
  }
1671
- throw new Error("unrecognized recipient key form");
1754
+ throw new HpkeError(
1755
+ "ERR_INVALID_RECIPIENT_KEY",
1756
+ "unrecognized recipient key form"
1757
+ );
1672
1758
  }
1673
1759
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1674
- function assertX25519Algorithm(key, context) {
1760
+ function assertRecipientPublicKey(key, context) {
1761
+ if (key.type !== "public") {
1762
+ throw new HpkeError(
1763
+ "ERR_INVALID_RECIPIENT_KEY",
1764
+ `${context} must be a public key`
1765
+ );
1766
+ }
1767
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1768
+ if (!key.extractable) {
1769
+ throw new HpkeError(
1770
+ "ERR_INVALID_RECIPIENT_KEY",
1771
+ `${context} must be extractable (the KEM exports it)`
1772
+ );
1773
+ }
1774
+ return key;
1775
+ }
1776
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1777
+ function assertX25519Algorithm(key, context, code2) {
1675
1778
  if (key.algorithm.name !== "X25519") {
1676
- throw new Error(
1779
+ throw new HpkeError(
1780
+ code2,
1677
1781
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1678
1782
  );
1679
1783
  }
1680
1784
  }
1681
1785
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1682
1786
  async function dh(priv, pub) {
1683
- const bits = await subtle2.deriveBits(
1684
- { name: "X25519", public: pub },
1685
- priv,
1686
- 256
1687
- );
1787
+ let bits;
1788
+ try {
1789
+ bits = await subtle2.deriveBits(
1790
+ { name: "X25519", public: pub },
1791
+ priv,
1792
+ 256
1793
+ );
1794
+ } catch (err) {
1795
+ if (err?.name === "OperationError") {
1796
+ throw new HpkeError(
1797
+ "ERR_SMALL_ORDER_KEY",
1798
+ "X25519 deriveBits rejected a small-order public key",
1799
+ { cause: err }
1800
+ );
1801
+ }
1802
+ throw err;
1803
+ }
1688
1804
  const bytes = new Uint8Array(bits);
1689
1805
  if (bytes.every((b) => b === 0)) {
1690
- throw new Error(
1806
+ throw new HpkeError(
1807
+ "ERR_SMALL_ORDER_KEY",
1691
1808
  "X25519 shared secret is all-zero (small-order public key?)"
1692
1809
  );
1693
1810
  }
@@ -1726,9 +1843,33 @@ async function encap(pkR) {
1726
1843
  __name(encap, "encap");
1727
1844
  async function decap(enc, keypair) {
1728
1845
  if (keypair.privateKey.type !== "private") {
1729
- throw new Error("keypair.privateKey must be a private key");
1846
+ throw new HpkeError(
1847
+ "ERR_INVALID_KEYPAIR",
1848
+ "keypair.privateKey must be a private key"
1849
+ );
1850
+ }
1851
+ assertX25519Algorithm(
1852
+ keypair.privateKey,
1853
+ "keypair.privateKey",
1854
+ "ERR_INVALID_KEYPAIR"
1855
+ );
1856
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1857
+ throw new HpkeError(
1858
+ "ERR_INVALID_KEYPAIR",
1859
+ "keypair.privateKey must have the deriveBits usage"
1860
+ );
1861
+ }
1862
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1863
+ throw new HpkeError(
1864
+ "ERR_INVALID_KEYPAIR",
1865
+ "keypair.publicKey must be an extractable public key"
1866
+ );
1730
1867
  }
1731
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1868
+ assertX25519Algorithm(
1869
+ keypair.publicKey,
1870
+ "keypair.publicKey",
1871
+ "ERR_INVALID_KEYPAIR"
1872
+ );
1732
1873
  const pkE = await subtle2.importKey(
1733
1874
  "raw",
1734
1875
  enc,
@@ -1742,6 +1883,45 @@ async function decap(enc, keypair) {
1742
1883
  return extractAndExpand(dhBytes, kemContext);
1743
1884
  }
1744
1885
  __name(decap, "decap");
1886
+ async function keyId(key) {
1887
+ if (key.algorithm.name !== "AES-GCM") {
1888
+ throw new HpkeError(
1889
+ "ERR_INVALID_AES_KEY",
1890
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1891
+ );
1892
+ }
1893
+ if (!key.usages.includes("encrypt")) {
1894
+ throw new HpkeError(
1895
+ "ERR_INVALID_AES_KEY",
1896
+ "keyId needs a key with the 'encrypt' usage"
1897
+ );
1898
+ }
1899
+ const labelHash = new Uint8Array(
1900
+ await subtle2.digest("SHA-256", KEY_ID_LABEL)
1901
+ );
1902
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1903
+ const ciphertext = new Uint8Array(await subtle2.encrypt(
1904
+ { name: "AES-GCM", iv: nonce },
1905
+ key,
1906
+ KEY_ID_LABEL
1907
+ ));
1908
+ const digest2 = await subtle2.digest(
1909
+ "SHA-256",
1910
+ concat2(KEY_ID_LABEL, ciphertext)
1911
+ );
1912
+ return toString2(new Uint8Array(digest2), "base64url");
1913
+ }
1914
+ __name(keyId, "keyId");
1915
+ async function verifyKeyId(key, expected) {
1916
+ const actual = await keyId(key);
1917
+ if (actual.length !== expected.length) return false;
1918
+ let diff = 0;
1919
+ for (let i = 0; i < actual.length; i++) {
1920
+ diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
1921
+ }
1922
+ return diff === 0;
1923
+ }
1924
+ __name(verifyKeyId, "verifyKeyId");
1745
1925
  function i2osp(n, len) {
1746
1926
  const out = new Uint8Array(len);
1747
1927
  let v = n;