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/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": 13195,
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": 14266,
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": 114255
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,32 +892,56 @@
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": 5825
899
+ "bytesInOutput": 5995
869
900
  },
870
901
  "src/util.ts": {
871
- "bytesInOutput": 6789
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": 51497
927
+ "bytes": 419
878
928
  },
879
929
  "dist/index.js.map": {
880
930
  "imports": [],
881
931
  "exports": [],
882
932
  "inputs": {},
883
- "bytes": 114246
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
- "open",
892
- "seal"
942
+ "encryptKey",
943
+ "keyId",
944
+ "open"
893
945
  ],
894
946
  "entryPoint": "src/index.ts",
895
947
  "inputs": {
@@ -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": 6776
1039
+ "bytesInOutput": 8110
985
1040
  },
986
1041
  "src/index.ts": {
987
- "bytesInOutput": 5830
1042
+ "bytesInOutput": 6166
988
1043
  }
989
1044
  },
990
- "bytes": 51378
1045
+ "bytes": 53408
991
1046
  },
992
1047
  "dist/util.js.map": {
993
1048
  "imports": [],
994
1049
  "exports": [],
995
1050
  "inputs": {},
996
- "bytes": 114252
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": 5825
1163
+ "bytesInOutput": 5995
1105
1164
  },
1106
1165
  "src/util.ts": {
1107
- "bytesInOutput": 6789
1166
+ "bytesInOutput": 8126
1108
1167
  }
1109
1168
  },
1110
- "bytes": 51505
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,19 +1350,11 @@ 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;
1342
- async function create2(recipient, opts) {
1343
- const { wrapped, key } = await encryptKey(recipient, null, opts);
1344
- return { enc: wrapped, key };
1345
- }
1346
- __name(create2, "create");
1347
- async function seal(recipient, aesKey, opts) {
1348
- const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1349
- return { enc: wrapped, key };
1350
- }
1351
- __name(seal, "seal");
1352
1358
  var open = Object.assign(openBytes, {
1353
1359
  raw: openRawBytes
1354
1360
  });
@@ -1361,13 +1367,19 @@ var decrypt = Object.assign(decryptBytes, {
1361
1367
  });
1362
1368
  async function decryptBytes(keypair, message, opts) {
1363
1369
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1364
- throw new Error("malformed message: too short");
1370
+ throw new HpkeError(
1371
+ "ERR_MALFORMED_MESSAGE",
1372
+ "malformed message: too short"
1373
+ );
1365
1374
  }
1366
1375
  const wrappedLen = message[0] << 8 | message[1];
1367
1376
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1368
1377
  const ctStart = ivStart + NN;
1369
1378
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1370
- throw new Error("malformed message: too short");
1379
+ throw new HpkeError(
1380
+ "ERR_MALFORMED_MESSAGE",
1381
+ "malformed message: too short"
1382
+ );
1371
1383
  }
1372
1384
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1373
1385
  const iv = message.slice(ivStart, ctStart);
@@ -1378,11 +1390,20 @@ async function decryptBytes(keypair, message, opts) {
1378
1390
  opts?.info !== void 0 ? { info: opts.info } : void 0
1379
1391
  );
1380
1392
  const key = await importAesKeyDecryptOnly(keyBytes);
1381
- const pt = await subtle.decrypt(
1382
- { name: "AES-GCM", iv },
1383
- key,
1384
- ciphertext
1385
- );
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
+ }
1386
1407
  return new Uint8Array(pt);
1387
1408
  }
1388
1409
  __name(decryptBytes, "decryptBytes");
@@ -1403,7 +1424,8 @@ async function decryptFromString(keypair, message, opts) {
1403
1424
  __name(decryptFromString, "decryptFromString");
1404
1425
  function validateKeysize(keysize) {
1405
1426
  if (keysize !== 128 && keysize !== 256) {
1406
- throw new Error(
1427
+ throw new HpkeError(
1428
+ "ERR_INVALID_KEYSIZE",
1407
1429
  `invalid keysize: ${keysize} (expected 128 or 256)`
1408
1430
  );
1409
1431
  }
@@ -1411,7 +1433,8 @@ function validateKeysize(keysize) {
1411
1433
  __name(validateKeysize, "validateKeysize");
1412
1434
  function validateRawKeyBytes(raw) {
1413
1435
  if (raw.length !== 16 && raw.length !== 32) {
1414
- throw new Error(
1436
+ throw new HpkeError(
1437
+ "ERR_INVALID_AES_KEY",
1415
1438
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1416
1439
  );
1417
1440
  }
@@ -1425,19 +1448,20 @@ function normalizeInfo(info) {
1425
1448
  __name(normalizeInfo, "normalizeInfo");
1426
1449
  async function exportAesKeyBytes(key) {
1427
1450
  if (!key.extractable) {
1428
- throw new Error(
1451
+ throw new HpkeError(
1452
+ "ERR_INVALID_AES_KEY",
1429
1453
  "aesKey must be extractable: its raw bytes are what get sealed"
1430
1454
  );
1431
1455
  }
1432
1456
  return new Uint8Array(await subtle.exportKey("raw", key));
1433
1457
  }
1434
1458
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1435
- async function importAesKey(raw) {
1459
+ async function importAesKey(raw, extractable) {
1436
1460
  return subtle.importKey(
1437
1461
  "raw",
1438
1462
  raw,
1439
1463
  { name: "AES-GCM" },
1440
- true,
1464
+ extractable,
1441
1465
  ["encrypt", "decrypt"]
1442
1466
  );
1443
1467
  }
@@ -1492,7 +1516,10 @@ async function keySchedule(sharedSecret, info) {
1492
1516
  __name(keySchedule, "keySchedule");
1493
1517
  async function openRawBytes(keypair, wrapped, opts) {
1494
1518
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1495
- throw new Error("malformed envelope: too short");
1519
+ throw new HpkeError(
1520
+ "ERR_MALFORMED_ENVELOPE",
1521
+ "malformed envelope: too short"
1522
+ );
1496
1523
  }
1497
1524
  const info = normalizeInfo(opts?.info);
1498
1525
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1504,7 +1531,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1504
1531
  __name(openRawBytes, "openRawBytes");
1505
1532
  async function openBytes(keypair, wrapped, opts) {
1506
1533
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1507
- return importAesKey(keyBytes);
1534
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1508
1535
  }
1509
1536
  __name(openBytes, "openBytes");
1510
1537
  async function encryptKey(recipient, aesKey, opts) {
@@ -1528,7 +1555,10 @@ async function encryptKey(recipient, aesKey, opts) {
1528
1555
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1529
1556
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1530
1557
  const wrapped = concat2(enc, ciphertext);
1531
- const aesGcmKey = await importAesKey(keyBytes);
1558
+ const aesGcmKey = await importAesKey(
1559
+ keyBytes,
1560
+ opts?.extractable ?? false
1561
+ );
1532
1562
  return { wrapped, key: aesGcmKey };
1533
1563
  }
1534
1564
  __name(encryptKey, "encryptKey");
@@ -1559,34 +1589,38 @@ async function aeadOpen(key, nonce, ciphertext) {
1559
1589
  false,
1560
1590
  ["decrypt"]
1561
1591
  );
1562
- const pt = await subtle2.decrypt(
1563
- { name: "AES-GCM", iv: nonce },
1564
- k,
1565
- ciphertext
1566
- );
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
+ }
1567
1606
  return new Uint8Array(pt);
1568
1607
  }
1569
1608
  __name(aeadOpen, "aeadOpen");
1570
1609
  async function encryptBytes(recipient, message, aesKey, opts) {
1571
1610
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1572
- let enc;
1573
- let key;
1574
- if (!aesKey) {
1575
- const keys = await create2(recipient, opts);
1576
- enc = keys.enc;
1577
- key = keys.key;
1578
- } else {
1579
- const keys = await seal(recipient, aesKey, opts);
1580
- enc = keys.enc;
1581
- key = keys.key;
1582
- }
1611
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1583
1612
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1584
1613
  const ct = new Uint8Array(await subtle2.encrypt(
1585
1614
  { name: "AES-GCM", iv },
1586
1615
  key,
1587
1616
  plaintext
1588
1617
  ));
1589
- 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
+ );
1590
1624
  }
1591
1625
  __name(encryptBytes, "encryptBytes");
1592
1626
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1652,7 +1686,8 @@ async function exportRawPublic(key) {
1652
1686
  __name(exportRawPublic, "exportRawPublic");
1653
1687
  async function importRawPublic(raw) {
1654
1688
  if (raw.length !== ENC_LENGTH) {
1655
- throw new Error(
1689
+ throw new HpkeError(
1690
+ "ERR_INVALID_RECIPIENT_KEY",
1656
1691
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1657
1692
  );
1658
1693
  }
@@ -1671,9 +1706,16 @@ async function resolveRecipientPublicKey(recipient) {
1671
1706
  }
1672
1707
  if (recipient instanceof CryptoKey) {
1673
1708
  if (recipient.type !== "public") {
1674
- 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
+ );
1675
1713
  }
1676
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1714
+ assertX25519Algorithm(
1715
+ recipient,
1716
+ "recipient CryptoKey",
1717
+ "ERR_INVALID_RECIPIENT_KEY"
1718
+ );
1677
1719
  return recipient;
1678
1720
  }
1679
1721
  if (typeof recipient.publicKey === "string") {
@@ -1683,17 +1725,28 @@ async function resolveRecipientPublicKey(recipient) {
1683
1725
  const pair = recipient;
1684
1726
  if (pair.publicKey instanceof CryptoKey) {
1685
1727
  if (pair.publicKey.type !== "public") {
1686
- 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
+ );
1687
1732
  }
1688
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1733
+ assertX25519Algorithm(
1734
+ pair.publicKey,
1735
+ "recipient keypair publicKey",
1736
+ "ERR_INVALID_RECIPIENT_KEY"
1737
+ );
1689
1738
  return pair.publicKey;
1690
1739
  }
1691
- throw new Error("unrecognized recipient key form");
1740
+ throw new HpkeError(
1741
+ "ERR_INVALID_RECIPIENT_KEY",
1742
+ "unrecognized recipient key form"
1743
+ );
1692
1744
  }
1693
1745
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1694
- function assertX25519Algorithm(key, context) {
1746
+ function assertX25519Algorithm(key, context, code2) {
1695
1747
  if (key.algorithm.name !== "X25519") {
1696
- throw new Error(
1748
+ throw new HpkeError(
1749
+ code2,
1697
1750
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1698
1751
  );
1699
1752
  }
@@ -1707,7 +1760,8 @@ async function dh(priv, pub) {
1707
1760
  );
1708
1761
  const bytes = new Uint8Array(bits);
1709
1762
  if (bytes.every((b) => b === 0)) {
1710
- throw new Error(
1763
+ throw new HpkeError(
1764
+ "ERR_SMALL_ORDER_KEY",
1711
1765
  "X25519 shared secret is all-zero (small-order public key?)"
1712
1766
  );
1713
1767
  }
@@ -1746,9 +1800,16 @@ async function encap(pkR) {
1746
1800
  __name(encap, "encap");
1747
1801
  async function decap(enc, keypair) {
1748
1802
  if (keypair.privateKey.type !== "private") {
1749
- 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
+ );
1750
1807
  }
1751
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1808
+ assertX25519Algorithm(
1809
+ keypair.privateKey,
1810
+ "keypair.privateKey",
1811
+ "ERR_INVALID_KEYPAIR"
1812
+ );
1752
1813
  const pkE = await subtle2.importKey(
1753
1814
  "raw",
1754
1815
  enc,
@@ -1762,6 +1823,35 @@ async function decap(enc, keypair) {
1762
1823
  return extractAndExpand(dhBytes, kemContext);
1763
1824
  }
1764
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");
1765
1855
  function i2osp(n, len) {
1766
1856
  const out = new Uint8Array(len);
1767
1857
  let v = n;