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/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": 14331,
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": 17940,
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": 121397
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,31 +892,55 @@
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": 5995
869
900
  },
870
901
  "src/util.ts": {
871
- "bytesInOutput": 6618
902
+ "bytesInOutput": 7314
872
903
  },
873
904
  "src/constants.ts": {
874
- "bytesInOutput": 452
905
+ "bytesInOutput": 551
906
+ }
907
+ },
908
+ "bytes": 52563
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": 122106
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",
943
+ "keyId",
892
944
  "open"
893
945
  ],
894
946
  "entryPoint": "src/index.ts",
@@ -977,23 +1029,26 @@
977
1029
  "node_modules/uint8arrays/dist/src/to-string.js": {
978
1030
  "bytesInOutput": 258
979
1031
  },
1032
+ "src/errors.ts": {
1033
+ "bytesInOutput": 215
1034
+ },
980
1035
  "src/constants.ts": {
981
- "bytesInOutput": 452
1036
+ "bytesInOutput": 551
982
1037
  },
983
1038
  "src/util.ts": {
984
- "bytesInOutput": 6605
1039
+ "bytesInOutput": 8110
985
1040
  },
986
1041
  "src/index.ts": {
987
- "bytesInOutput": 5649
1042
+ "bytesInOutput": 6166
988
1043
  }
989
1044
  },
990
- "bytes": 51032
1045
+ "bytes": 53408
991
1046
  },
992
1047
  "dist/util.js.map": {
993
1048
  "imports": [],
994
1049
  "exports": [],
995
1050
  "inputs": {},
996
- "bytes": 112937
1051
+ "bytes": 121973
997
1052
  },
998
1053
  "dist/util.js": {
999
1054
  "imports": [],
@@ -1007,6 +1062,7 @@
1007
1062
  "encryptToString",
1008
1063
  "hmac",
1009
1064
  "i2osp",
1065
+ "keyId",
1010
1066
  "labeledExpand",
1011
1067
  "labeledExtract",
1012
1068
  "resolveRecipientPublicKey"
@@ -1097,17 +1153,20 @@
1097
1153
  "node_modules/uint8arrays/dist/src/to-string.js": {
1098
1154
  "bytesInOutput": 258
1099
1155
  },
1156
+ "src/errors.ts": {
1157
+ "bytesInOutput": 215
1158
+ },
1100
1159
  "src/constants.ts": {
1101
- "bytesInOutput": 452
1160
+ "bytesInOutput": 551
1102
1161
  },
1103
1162
  "src/index.ts": {
1104
- "bytesInOutput": 5486
1163
+ "bytesInOutput": 5995
1105
1164
  },
1106
1165
  "src/util.ts": {
1107
- "bytesInOutput": 6618
1166
+ "bytesInOutput": 8126
1108
1167
  }
1109
1168
  },
1110
- "bytes": 50995
1169
+ "bytes": 53353
1111
1170
  }
1112
1171
  }
1113
1172
  }
package/dist/util.cjs CHANGED
@@ -30,6 +30,7 @@ __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
36
  resolveRecipientPublicKey: () => resolveRecipientPublicKey
@@ -1314,6 +1315,19 @@ function toString2(array, encoding = "utf8") {
1314
1315
  }
1315
1316
  __name(toString2, "toString");
1316
1317
 
1318
+ // src/errors.ts
1319
+ var HpkeError = class extends Error {
1320
+ static {
1321
+ __name(this, "HpkeError");
1322
+ }
1323
+ code;
1324
+ constructor(code2, message, opts) {
1325
+ super(message, opts);
1326
+ this.name = "HpkeError";
1327
+ this.code = code2;
1328
+ }
1329
+ };
1330
+
1317
1331
  // src/constants.ts
1318
1332
  var KEM_ID = 32;
1319
1333
  var KDF_ID = 1;
@@ -1336,6 +1350,8 @@ var HPKE_SUITE_ID = concat2(
1336
1350
  i2osp(KDF_ID, 2),
1337
1351
  i2osp(AEAD_ID, 2)
1338
1352
  );
1353
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1354
+ var KEY_ID_NONCE_LENGTH = 12;
1339
1355
 
1340
1356
  // src/index.ts
1341
1357
  var subtle = globalThis.crypto.subtle;
@@ -1351,13 +1367,19 @@ var decrypt = Object.assign(decryptBytes, {
1351
1367
  });
1352
1368
  async function decryptBytes(keypair, message, opts) {
1353
1369
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1354
- throw new Error("malformed message: too short");
1370
+ throw new HpkeError(
1371
+ "ERR_MALFORMED_MESSAGE",
1372
+ "malformed message: too short"
1373
+ );
1355
1374
  }
1356
1375
  const wrappedLen = message[0] << 8 | message[1];
1357
1376
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1358
1377
  const ctStart = ivStart + NN;
1359
1378
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1360
- throw new Error("malformed message: too short");
1379
+ throw new HpkeError(
1380
+ "ERR_MALFORMED_MESSAGE",
1381
+ "malformed message: too short"
1382
+ );
1361
1383
  }
1362
1384
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1363
1385
  const iv = message.slice(ivStart, ctStart);
@@ -1368,11 +1390,20 @@ async function decryptBytes(keypair, message, opts) {
1368
1390
  opts?.info !== void 0 ? { info: opts.info } : void 0
1369
1391
  );
1370
1392
  const key = await importAesKeyDecryptOnly(keyBytes);
1371
- const pt = await subtle.decrypt(
1372
- { name: "AES-GCM", iv },
1373
- key,
1374
- ciphertext
1375
- );
1393
+ let pt;
1394
+ try {
1395
+ pt = await subtle.decrypt(
1396
+ { name: "AES-GCM", iv },
1397
+ key,
1398
+ ciphertext
1399
+ );
1400
+ } catch (err) {
1401
+ throw new HpkeError(
1402
+ "ERR_DECRYPT_FAILED",
1403
+ "failed to decrypt the message: tampered ciphertext",
1404
+ { cause: err }
1405
+ );
1406
+ }
1376
1407
  return new Uint8Array(pt);
1377
1408
  }
1378
1409
  __name(decryptBytes, "decryptBytes");
@@ -1393,7 +1424,8 @@ async function decryptFromString(keypair, message, opts) {
1393
1424
  __name(decryptFromString, "decryptFromString");
1394
1425
  function validateKeysize(keysize) {
1395
1426
  if (keysize !== 128 && keysize !== 256) {
1396
- throw new Error(
1427
+ throw new HpkeError(
1428
+ "ERR_INVALID_KEYSIZE",
1397
1429
  `invalid keysize: ${keysize} (expected 128 or 256)`
1398
1430
  );
1399
1431
  }
@@ -1401,7 +1433,8 @@ function validateKeysize(keysize) {
1401
1433
  __name(validateKeysize, "validateKeysize");
1402
1434
  function validateRawKeyBytes(raw) {
1403
1435
  if (raw.length !== 16 && raw.length !== 32) {
1404
- throw new Error(
1436
+ throw new HpkeError(
1437
+ "ERR_INVALID_AES_KEY",
1405
1438
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1406
1439
  );
1407
1440
  }
@@ -1415,19 +1448,20 @@ function normalizeInfo(info) {
1415
1448
  __name(normalizeInfo, "normalizeInfo");
1416
1449
  async function exportAesKeyBytes(key) {
1417
1450
  if (!key.extractable) {
1418
- throw new Error(
1451
+ throw new HpkeError(
1452
+ "ERR_INVALID_AES_KEY",
1419
1453
  "aesKey must be extractable: its raw bytes are what get sealed"
1420
1454
  );
1421
1455
  }
1422
1456
  return new Uint8Array(await subtle.exportKey("raw", key));
1423
1457
  }
1424
1458
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1425
- async function importAesKey(raw) {
1459
+ async function importAesKey(raw, extractable) {
1426
1460
  return subtle.importKey(
1427
1461
  "raw",
1428
1462
  raw,
1429
1463
  { name: "AES-GCM" },
1430
- true,
1464
+ extractable,
1431
1465
  ["encrypt", "decrypt"]
1432
1466
  );
1433
1467
  }
@@ -1482,7 +1516,10 @@ async function keySchedule(sharedSecret, info) {
1482
1516
  __name(keySchedule, "keySchedule");
1483
1517
  async function openRawBytes(keypair, wrapped, opts) {
1484
1518
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1485
- throw new Error("malformed envelope: too short");
1519
+ throw new HpkeError(
1520
+ "ERR_MALFORMED_ENVELOPE",
1521
+ "malformed envelope: too short"
1522
+ );
1486
1523
  }
1487
1524
  const info = normalizeInfo(opts?.info);
1488
1525
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1494,7 +1531,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1494
1531
  __name(openRawBytes, "openRawBytes");
1495
1532
  async function openBytes(keypair, wrapped, opts) {
1496
1533
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1497
- return importAesKey(keyBytes);
1534
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1498
1535
  }
1499
1536
  __name(openBytes, "openBytes");
1500
1537
  async function encryptKey(recipient, aesKey, opts) {
@@ -1518,8 +1555,11 @@ async function encryptKey(recipient, aesKey, opts) {
1518
1555
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1519
1556
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1520
1557
  const wrapped = concat2(enc, ciphertext);
1521
- const aesGcmKey = await importAesKey(keyBytes);
1522
- return { enc: wrapped, key: aesGcmKey };
1558
+ const aesGcmKey = await importAesKey(
1559
+ keyBytes,
1560
+ opts?.extractable ?? false
1561
+ );
1562
+ return { wrapped, key: aesGcmKey };
1523
1563
  }
1524
1564
  __name(encryptKey, "encryptKey");
1525
1565
 
@@ -1549,24 +1589,38 @@ async function aeadOpen(key, nonce, ciphertext) {
1549
1589
  false,
1550
1590
  ["decrypt"]
1551
1591
  );
1552
- const pt = await subtle2.decrypt(
1553
- { name: "AES-GCM", iv: nonce },
1554
- k,
1555
- ciphertext
1556
- );
1592
+ let pt;
1593
+ try {
1594
+ pt = await subtle2.decrypt(
1595
+ { name: "AES-GCM", iv: nonce },
1596
+ k,
1597
+ ciphertext
1598
+ );
1599
+ } catch (err) {
1600
+ throw new HpkeError(
1601
+ "ERR_DECRYPT_FAILED",
1602
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1603
+ { cause: err }
1604
+ );
1605
+ }
1557
1606
  return new Uint8Array(pt);
1558
1607
  }
1559
1608
  __name(aeadOpen, "aeadOpen");
1560
1609
  async function encryptBytes(recipient, message, aesKey, opts) {
1561
1610
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1562
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1611
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1563
1612
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1564
1613
  const ct = new Uint8Array(await subtle2.encrypt(
1565
1614
  { name: "AES-GCM", iv },
1566
1615
  key,
1567
1616
  plaintext
1568
1617
  ));
1569
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1618
+ return concat2(
1619
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1620
+ wrapped,
1621
+ iv,
1622
+ ct
1623
+ );
1570
1624
  }
1571
1625
  __name(encryptBytes, "encryptBytes");
1572
1626
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1632,7 +1686,8 @@ async function exportRawPublic(key) {
1632
1686
  __name(exportRawPublic, "exportRawPublic");
1633
1687
  async function importRawPublic(raw) {
1634
1688
  if (raw.length !== ENC_LENGTH) {
1635
- throw new Error(
1689
+ throw new HpkeError(
1690
+ "ERR_INVALID_RECIPIENT_KEY",
1636
1691
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1637
1692
  );
1638
1693
  }
@@ -1651,9 +1706,16 @@ async function resolveRecipientPublicKey(recipient) {
1651
1706
  }
1652
1707
  if (recipient instanceof CryptoKey) {
1653
1708
  if (recipient.type !== "public") {
1654
- throw new Error("recipient CryptoKey must be a public key");
1709
+ throw new HpkeError(
1710
+ "ERR_INVALID_RECIPIENT_KEY",
1711
+ "recipient CryptoKey must be a public key"
1712
+ );
1655
1713
  }
1656
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1714
+ assertX25519Algorithm(
1715
+ recipient,
1716
+ "recipient CryptoKey",
1717
+ "ERR_INVALID_RECIPIENT_KEY"
1718
+ );
1657
1719
  return recipient;
1658
1720
  }
1659
1721
  if (typeof recipient.publicKey === "string") {
@@ -1663,17 +1725,28 @@ async function resolveRecipientPublicKey(recipient) {
1663
1725
  const pair = recipient;
1664
1726
  if (pair.publicKey instanceof CryptoKey) {
1665
1727
  if (pair.publicKey.type !== "public") {
1666
- throw new Error("recipient keypair publicKey must be a public key");
1728
+ throw new HpkeError(
1729
+ "ERR_INVALID_RECIPIENT_KEY",
1730
+ "recipient keypair publicKey must be a public key"
1731
+ );
1667
1732
  }
1668
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1733
+ assertX25519Algorithm(
1734
+ pair.publicKey,
1735
+ "recipient keypair publicKey",
1736
+ "ERR_INVALID_RECIPIENT_KEY"
1737
+ );
1669
1738
  return pair.publicKey;
1670
1739
  }
1671
- throw new Error("unrecognized recipient key form");
1740
+ throw new HpkeError(
1741
+ "ERR_INVALID_RECIPIENT_KEY",
1742
+ "unrecognized recipient key form"
1743
+ );
1672
1744
  }
1673
1745
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1674
- function assertX25519Algorithm(key, context) {
1746
+ function assertX25519Algorithm(key, context, code2) {
1675
1747
  if (key.algorithm.name !== "X25519") {
1676
- throw new Error(
1748
+ throw new HpkeError(
1749
+ code2,
1677
1750
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1678
1751
  );
1679
1752
  }
@@ -1687,7 +1760,8 @@ async function dh(priv, pub) {
1687
1760
  );
1688
1761
  const bytes = new Uint8Array(bits);
1689
1762
  if (bytes.every((b) => b === 0)) {
1690
- throw new Error(
1763
+ throw new HpkeError(
1764
+ "ERR_SMALL_ORDER_KEY",
1691
1765
  "X25519 shared secret is all-zero (small-order public key?)"
1692
1766
  );
1693
1767
  }
@@ -1726,9 +1800,16 @@ async function encap(pkR) {
1726
1800
  __name(encap, "encap");
1727
1801
  async function decap(enc, keypair) {
1728
1802
  if (keypair.privateKey.type !== "private") {
1729
- throw new Error("keypair.privateKey must be a private key");
1803
+ throw new HpkeError(
1804
+ "ERR_INVALID_KEYPAIR",
1805
+ "keypair.privateKey must be a private key"
1806
+ );
1730
1807
  }
1731
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1808
+ assertX25519Algorithm(
1809
+ keypair.privateKey,
1810
+ "keypair.privateKey",
1811
+ "ERR_INVALID_KEYPAIR"
1812
+ );
1732
1813
  const pkE = await subtle2.importKey(
1733
1814
  "raw",
1734
1815
  enc,
@@ -1742,6 +1823,35 @@ async function decap(enc, keypair) {
1742
1823
  return extractAndExpand(dhBytes, kemContext);
1743
1824
  }
1744
1825
  __name(decap, "decap");
1826
+ async function keyId(key) {
1827
+ if (key.algorithm.name !== "AES-GCM") {
1828
+ throw new HpkeError(
1829
+ "ERR_INVALID_AES_KEY",
1830
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1831
+ );
1832
+ }
1833
+ if (!key.usages.includes("encrypt")) {
1834
+ throw new HpkeError(
1835
+ "ERR_INVALID_AES_KEY",
1836
+ "keyId needs a key with the 'encrypt' usage"
1837
+ );
1838
+ }
1839
+ const labelHash = new Uint8Array(
1840
+ await subtle2.digest("SHA-256", KEY_ID_LABEL)
1841
+ );
1842
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1843
+ const ciphertext = new Uint8Array(await subtle2.encrypt(
1844
+ { name: "AES-GCM", iv: nonce },
1845
+ key,
1846
+ KEY_ID_LABEL
1847
+ ));
1848
+ const digest2 = await subtle2.digest(
1849
+ "SHA-256",
1850
+ concat2(KEY_ID_LABEL, ciphertext)
1851
+ );
1852
+ return toString2(new Uint8Array(digest2), "base64url");
1853
+ }
1854
+ __name(keyId, "keyId");
1745
1855
  function i2osp(n, len) {
1746
1856
  const out = new Uint8Array(len);
1747
1857
  let v = n;