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.
@@ -29,6 +29,8 @@ __export(constants_exports, {
29
29
  KDF_ID: () => KDF_ID,
30
30
  KEM_ID: () => KEM_ID,
31
31
  KEM_SUITE_ID: () => KEM_SUITE_ID,
32
+ KEY_ID_LABEL: () => KEY_ID_LABEL,
33
+ KEY_ID_NONCE_LENGTH: () => KEY_ID_NONCE_LENGTH,
32
34
  MODE_BASE: () => MODE_BASE,
33
35
  NK: () => NK,
34
36
  NN: () => NN,
@@ -1315,6 +1317,19 @@ function toString2(array, encoding = "utf8") {
1315
1317
  }
1316
1318
  __name(toString2, "toString");
1317
1319
 
1320
+ // src/errors.ts
1321
+ var HpkeError = class extends Error {
1322
+ static {
1323
+ __name(this, "HpkeError");
1324
+ }
1325
+ code;
1326
+ constructor(code2, message, opts) {
1327
+ super(message, opts);
1328
+ this.name = "HpkeError";
1329
+ this.code = code2;
1330
+ }
1331
+ };
1332
+
1318
1333
  // src/index.ts
1319
1334
  var subtle = globalThis.crypto.subtle;
1320
1335
  var open = Object.assign(openBytes, {
@@ -1329,13 +1344,19 @@ var decrypt = Object.assign(decryptBytes, {
1329
1344
  });
1330
1345
  async function decryptBytes(keypair, message, opts) {
1331
1346
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1332
- throw new Error("malformed message: too short");
1347
+ throw new HpkeError(
1348
+ "ERR_MALFORMED_MESSAGE",
1349
+ "malformed message: too short"
1350
+ );
1333
1351
  }
1334
1352
  const wrappedLen = message[0] << 8 | message[1];
1335
1353
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1336
1354
  const ctStart = ivStart + NN;
1337
1355
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1338
- throw new Error("malformed message: too short");
1356
+ throw new HpkeError(
1357
+ "ERR_MALFORMED_MESSAGE",
1358
+ "malformed message: too short"
1359
+ );
1339
1360
  }
1340
1361
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1341
1362
  const iv = message.slice(ivStart, ctStart);
@@ -1346,11 +1367,20 @@ async function decryptBytes(keypair, message, opts) {
1346
1367
  opts?.info !== void 0 ? { info: opts.info } : void 0
1347
1368
  );
1348
1369
  const key = await importAesKeyDecryptOnly(keyBytes);
1349
- const pt = await subtle.decrypt(
1350
- { name: "AES-GCM", iv },
1351
- key,
1352
- ciphertext
1353
- );
1370
+ let pt;
1371
+ try {
1372
+ pt = await subtle.decrypt(
1373
+ { name: "AES-GCM", iv },
1374
+ key,
1375
+ ciphertext
1376
+ );
1377
+ } catch (err) {
1378
+ throw new HpkeError(
1379
+ "ERR_DECRYPT_FAILED",
1380
+ "failed to decrypt the message: tampered ciphertext",
1381
+ { cause: err }
1382
+ );
1383
+ }
1354
1384
  return new Uint8Array(pt);
1355
1385
  }
1356
1386
  __name(decryptBytes, "decryptBytes");
@@ -1360,7 +1390,16 @@ async function decryptToString(keypair, message, opts) {
1360
1390
  }
1361
1391
  __name(decryptToString, "decryptToString");
1362
1392
  async function decryptFromString(keypair, message, opts) {
1363
- const bytes = fromString2(message, "base64url");
1393
+ let bytes;
1394
+ try {
1395
+ bytes = fromString2(message, "base64url");
1396
+ } catch (err) {
1397
+ throw new HpkeError(
1398
+ "ERR_MALFORMED_MESSAGE",
1399
+ "malformed message: not base64url",
1400
+ { cause: err }
1401
+ );
1402
+ }
1364
1403
  const plaintext = await decryptBytes(
1365
1404
  keypair,
1366
1405
  bytes,
@@ -1371,7 +1410,8 @@ async function decryptFromString(keypair, message, opts) {
1371
1410
  __name(decryptFromString, "decryptFromString");
1372
1411
  function validateKeysize(keysize) {
1373
1412
  if (keysize !== 128 && keysize !== 256) {
1374
- throw new Error(
1413
+ throw new HpkeError(
1414
+ "ERR_INVALID_KEYSIZE",
1375
1415
  `invalid keysize: ${keysize} (expected 128 or 256)`
1376
1416
  );
1377
1417
  }
@@ -1379,7 +1419,8 @@ function validateKeysize(keysize) {
1379
1419
  __name(validateKeysize, "validateKeysize");
1380
1420
  function validateRawKeyBytes(raw) {
1381
1421
  if (raw.length !== 16 && raw.length !== 32) {
1382
- throw new Error(
1422
+ throw new HpkeError(
1423
+ "ERR_INVALID_AES_KEY",
1383
1424
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1384
1425
  );
1385
1426
  }
@@ -1393,19 +1434,20 @@ function normalizeInfo(info) {
1393
1434
  __name(normalizeInfo, "normalizeInfo");
1394
1435
  async function exportAesKeyBytes(key) {
1395
1436
  if (!key.extractable) {
1396
- throw new Error(
1437
+ throw new HpkeError(
1438
+ "ERR_INVALID_AES_KEY",
1397
1439
  "aesKey must be extractable: its raw bytes are what get sealed"
1398
1440
  );
1399
1441
  }
1400
1442
  return new Uint8Array(await subtle.exportKey("raw", key));
1401
1443
  }
1402
1444
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1403
- async function importAesKey(raw) {
1445
+ async function importAesKey(raw, extractable) {
1404
1446
  return subtle.importKey(
1405
1447
  "raw",
1406
1448
  raw,
1407
1449
  { name: "AES-GCM" },
1408
- true,
1450
+ extractable,
1409
1451
  ["encrypt", "decrypt"]
1410
1452
  );
1411
1453
  }
@@ -1460,7 +1502,10 @@ async function keySchedule(sharedSecret, info) {
1460
1502
  __name(keySchedule, "keySchedule");
1461
1503
  async function openRawBytes(keypair, wrapped, opts) {
1462
1504
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1463
- throw new Error("malformed envelope: too short");
1505
+ throw new HpkeError(
1506
+ "ERR_MALFORMED_ENVELOPE",
1507
+ "malformed envelope: too short"
1508
+ );
1464
1509
  }
1465
1510
  const info = normalizeInfo(opts?.info);
1466
1511
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1472,7 +1517,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1472
1517
  __name(openRawBytes, "openRawBytes");
1473
1518
  async function openBytes(keypair, wrapped, opts) {
1474
1519
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1475
- return importAesKey(keyBytes);
1520
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1476
1521
  }
1477
1522
  __name(openBytes, "openBytes");
1478
1523
  async function encryptKey(recipient, aesKey, opts) {
@@ -1482,6 +1527,12 @@ async function encryptKey(recipient, aesKey, opts) {
1482
1527
  validateRawKeyBytes(aesKey);
1483
1528
  keyBytes = aesKey;
1484
1529
  } else if (aesKey) {
1530
+ if (aesKey.algorithm.name !== "AES-GCM") {
1531
+ throw new HpkeError(
1532
+ "ERR_INVALID_AES_KEY",
1533
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1534
+ );
1535
+ }
1485
1536
  keyBytes = await exportAesKeyBytes(aesKey);
1486
1537
  validateRawKeyBytes(keyBytes);
1487
1538
  } else {
@@ -1496,8 +1547,12 @@ async function encryptKey(recipient, aesKey, opts) {
1496
1547
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1497
1548
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1498
1549
  const wrapped = concat2(enc, ciphertext);
1499
- const aesGcmKey = await importAesKey(keyBytes);
1500
- return { enc: wrapped, key: aesGcmKey };
1550
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1551
+ const aesGcmKey = await importAesKey(
1552
+ keyBytes,
1553
+ opts?.extractable ?? false
1554
+ );
1555
+ return { wrapped, key: aesGcmKey };
1501
1556
  }
1502
1557
  __name(encryptKey, "encryptKey");
1503
1558
 
@@ -1527,24 +1582,44 @@ async function aeadOpen(key, nonce, ciphertext) {
1527
1582
  false,
1528
1583
  ["decrypt"]
1529
1584
  );
1530
- const pt = await subtle2.decrypt(
1531
- { name: "AES-GCM", iv: nonce },
1532
- k,
1533
- ciphertext
1534
- );
1585
+ let pt;
1586
+ try {
1587
+ pt = await subtle2.decrypt(
1588
+ { name: "AES-GCM", iv: nonce },
1589
+ k,
1590
+ ciphertext
1591
+ );
1592
+ } catch (err) {
1593
+ throw new HpkeError(
1594
+ "ERR_DECRYPT_FAILED",
1595
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1596
+ { cause: err }
1597
+ );
1598
+ }
1535
1599
  return new Uint8Array(pt);
1536
1600
  }
1537
1601
  __name(aeadOpen, "aeadOpen");
1538
1602
  async function encryptBytes(recipient, message, aesKey, opts) {
1603
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1604
+ throw new HpkeError(
1605
+ "ERR_INVALID_AES_KEY",
1606
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1607
+ );
1608
+ }
1539
1609
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1540
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1610
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1541
1611
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1542
1612
  const ct = new Uint8Array(await subtle2.encrypt(
1543
1613
  { name: "AES-GCM", iv },
1544
1614
  key,
1545
1615
  plaintext
1546
1616
  ));
1547
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1617
+ return concat2(
1618
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1619
+ wrapped,
1620
+ iv,
1621
+ ct
1622
+ );
1548
1623
  }
1549
1624
  __name(encryptBytes, "encryptBytes");
1550
1625
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1610,7 +1685,8 @@ async function exportRawPublic(key) {
1610
1685
  __name(exportRawPublic, "exportRawPublic");
1611
1686
  async function importRawPublic(raw) {
1612
1687
  if (raw.length !== ENC_LENGTH) {
1613
- throw new Error(
1688
+ throw new HpkeError(
1689
+ "ERR_INVALID_RECIPIENT_KEY",
1614
1690
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1615
1691
  );
1616
1692
  }
@@ -1628,44 +1704,83 @@ async function resolveRecipientPublicKey(recipient) {
1628
1704
  return importRawPublic(recipient);
1629
1705
  }
1630
1706
  if (recipient instanceof CryptoKey) {
1631
- if (recipient.type !== "public") {
1632
- throw new Error("recipient CryptoKey must be a public key");
1633
- }
1634
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1635
- return recipient;
1707
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1636
1708
  }
1637
1709
  if (typeof recipient.publicKey === "string") {
1638
1710
  const { publicKey, encoding } = recipient;
1639
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1711
+ let decoded;
1712
+ try {
1713
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1714
+ } catch (err) {
1715
+ throw new HpkeError(
1716
+ "ERR_INVALID_RECIPIENT_KEY",
1717
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1718
+ { cause: err }
1719
+ );
1720
+ }
1721
+ return importRawPublic(decoded);
1640
1722
  }
1641
1723
  const pair = recipient;
1642
1724
  if (pair.publicKey instanceof CryptoKey) {
1643
- if (pair.publicKey.type !== "public") {
1644
- throw new Error("recipient keypair publicKey must be a public key");
1645
- }
1646
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1647
- return pair.publicKey;
1725
+ return assertRecipientPublicKey(
1726
+ pair.publicKey,
1727
+ "recipient keypair publicKey"
1728
+ );
1648
1729
  }
1649
- throw new Error("unrecognized recipient key form");
1730
+ throw new HpkeError(
1731
+ "ERR_INVALID_RECIPIENT_KEY",
1732
+ "unrecognized recipient key form"
1733
+ );
1650
1734
  }
1651
1735
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1652
- function assertX25519Algorithm(key, context) {
1736
+ function assertRecipientPublicKey(key, context) {
1737
+ if (key.type !== "public") {
1738
+ throw new HpkeError(
1739
+ "ERR_INVALID_RECIPIENT_KEY",
1740
+ `${context} must be a public key`
1741
+ );
1742
+ }
1743
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1744
+ if (!key.extractable) {
1745
+ throw new HpkeError(
1746
+ "ERR_INVALID_RECIPIENT_KEY",
1747
+ `${context} must be extractable (the KEM exports it)`
1748
+ );
1749
+ }
1750
+ return key;
1751
+ }
1752
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1753
+ function assertX25519Algorithm(key, context, code2) {
1653
1754
  if (key.algorithm.name !== "X25519") {
1654
- throw new Error(
1755
+ throw new HpkeError(
1756
+ code2,
1655
1757
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1656
1758
  );
1657
1759
  }
1658
1760
  }
1659
1761
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1660
1762
  async function dh(priv, pub) {
1661
- const bits = await subtle2.deriveBits(
1662
- { name: "X25519", public: pub },
1663
- priv,
1664
- 256
1665
- );
1763
+ let bits;
1764
+ try {
1765
+ bits = await subtle2.deriveBits(
1766
+ { name: "X25519", public: pub },
1767
+ priv,
1768
+ 256
1769
+ );
1770
+ } catch (err) {
1771
+ if (err?.name === "OperationError") {
1772
+ throw new HpkeError(
1773
+ "ERR_SMALL_ORDER_KEY",
1774
+ "X25519 deriveBits rejected a small-order public key",
1775
+ { cause: err }
1776
+ );
1777
+ }
1778
+ throw err;
1779
+ }
1666
1780
  const bytes = new Uint8Array(bits);
1667
1781
  if (bytes.every((b) => b === 0)) {
1668
- throw new Error(
1782
+ throw new HpkeError(
1783
+ "ERR_SMALL_ORDER_KEY",
1669
1784
  "X25519 shared secret is all-zero (small-order public key?)"
1670
1785
  );
1671
1786
  }
@@ -1704,9 +1819,33 @@ async function encap(pkR) {
1704
1819
  __name(encap, "encap");
1705
1820
  async function decap(enc, keypair) {
1706
1821
  if (keypair.privateKey.type !== "private") {
1707
- throw new Error("keypair.privateKey must be a private key");
1822
+ throw new HpkeError(
1823
+ "ERR_INVALID_KEYPAIR",
1824
+ "keypair.privateKey must be a private key"
1825
+ );
1708
1826
  }
1709
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1827
+ assertX25519Algorithm(
1828
+ keypair.privateKey,
1829
+ "keypair.privateKey",
1830
+ "ERR_INVALID_KEYPAIR"
1831
+ );
1832
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1833
+ throw new HpkeError(
1834
+ "ERR_INVALID_KEYPAIR",
1835
+ "keypair.privateKey must have the deriveBits usage"
1836
+ );
1837
+ }
1838
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1839
+ throw new HpkeError(
1840
+ "ERR_INVALID_KEYPAIR",
1841
+ "keypair.publicKey must be an extractable public key"
1842
+ );
1843
+ }
1844
+ assertX25519Algorithm(
1845
+ keypair.publicKey,
1846
+ "keypair.publicKey",
1847
+ "ERR_INVALID_KEYPAIR"
1848
+ );
1710
1849
  const pkE = await subtle2.importKey(
1711
1850
  "raw",
1712
1851
  enc,
@@ -1765,4 +1904,6 @@ var HPKE_SUITE_ID = concat2(
1765
1904
  i2osp(KDF_ID, 2),
1766
1905
  i2osp(AEAD_ID, 2)
1767
1906
  );
1907
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1908
+ var KEY_ID_NONCE_LENGTH = 12;
1768
1909
  //# sourceMappingURL=constants.cjs.map