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.
@@ -11,4 +11,6 @@ export declare const HPKE_V1: Uint8Array<ArrayBuffer>;
11
11
  export declare const WRAPPED_LEN_PREFIX = 2;
12
12
  export declare const KEM_SUITE_ID: Uint8Array<ArrayBufferLike>;
13
13
  export declare const HPKE_SUITE_ID: Uint8Array<ArrayBufferLike>;
14
+ export declare const KEY_ID_LABEL: Uint8Array<ArrayBuffer>;
15
+ export declare const KEY_ID_NONCE_LENGTH = 12;
14
16
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,MAAM,KAAS,CAAA;AAC5B,eAAO,MAAM,MAAM,IAAS,CAAA;AAC5B,eAAO,MAAM,OAAO,IAAS,CAAA;AAI7B,eAAO,MAAM,OAAO,KAAK,CAAA;AACzB,eAAO,MAAM,EAAE,KAAK,CAAA;AACpB,eAAO,MAAM,EAAE,KAAK,CAAA;AACpB,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,eAAO,MAAM,eAAe,KAAK,CAAA;AAGjC,eAAO,MAAM,SAAS,IAAO,CAAA;AAE7B,eAAO,MAAM,OAAO,yBAAsC,CAAA;AAK1D,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAGnC,eAAO,MAAM,YAAY,6BAGxB,CAAA;AAID,eAAO,MAAM,aAAa,6BAKzB,CAAA"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,MAAM,KAAS,CAAA;AAC5B,eAAO,MAAM,MAAM,IAAS,CAAA;AAC5B,eAAO,MAAM,OAAO,IAAS,CAAA;AAI7B,eAAO,MAAM,OAAO,KAAK,CAAA;AACzB,eAAO,MAAM,EAAE,KAAK,CAAA;AACpB,eAAO,MAAM,EAAE,KAAK,CAAA;AACpB,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,eAAO,MAAM,eAAe,KAAK,CAAA;AAGjC,eAAO,MAAM,SAAS,IAAO,CAAA;AAE7B,eAAO,MAAM,OAAO,yBAAsC,CAAA;AAK1D,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAGnC,eAAO,MAAM,YAAY,6BAGxB,CAAA;AAID,eAAO,MAAM,aAAa,6BAKzB,CAAA;AAUD,eAAO,MAAM,YAAY,yBACU,CAAA;AAMnC,eAAO,MAAM,mBAAmB,KAAK,CAAA"}
package/dist/constants.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/index.ts
1287
1300
  var subtle = globalThis.crypto.subtle;
1288
1301
  var open = Object.assign(openBytes, {
@@ -1297,13 +1310,19 @@ var decrypt = Object.assign(decryptBytes, {
1297
1310
  });
1298
1311
  async function decryptBytes(keypair, message, opts) {
1299
1312
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1300
- throw new Error("malformed message: too short");
1313
+ throw new HpkeError(
1314
+ "ERR_MALFORMED_MESSAGE",
1315
+ "malformed message: too short"
1316
+ );
1301
1317
  }
1302
1318
  const wrappedLen = message[0] << 8 | message[1];
1303
1319
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1304
1320
  const ctStart = ivStart + NN;
1305
1321
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1306
- throw new Error("malformed message: too short");
1322
+ throw new HpkeError(
1323
+ "ERR_MALFORMED_MESSAGE",
1324
+ "malformed message: too short"
1325
+ );
1307
1326
  }
1308
1327
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1309
1328
  const iv = message.slice(ivStart, ctStart);
@@ -1314,11 +1333,20 @@ async function decryptBytes(keypair, message, opts) {
1314
1333
  opts?.info !== void 0 ? { info: opts.info } : void 0
1315
1334
  );
1316
1335
  const key = await importAesKeyDecryptOnly(keyBytes);
1317
- const pt = await subtle.decrypt(
1318
- { name: "AES-GCM", iv },
1319
- key,
1320
- ciphertext
1321
- );
1336
+ let pt;
1337
+ try {
1338
+ pt = await subtle.decrypt(
1339
+ { name: "AES-GCM", iv },
1340
+ key,
1341
+ ciphertext
1342
+ );
1343
+ } catch (err) {
1344
+ throw new HpkeError(
1345
+ "ERR_DECRYPT_FAILED",
1346
+ "failed to decrypt the message: tampered ciphertext",
1347
+ { cause: err }
1348
+ );
1349
+ }
1322
1350
  return new Uint8Array(pt);
1323
1351
  }
1324
1352
  __name(decryptBytes, "decryptBytes");
@@ -1328,7 +1356,16 @@ async function decryptToString(keypair, message, opts) {
1328
1356
  }
1329
1357
  __name(decryptToString, "decryptToString");
1330
1358
  async function decryptFromString(keypair, message, opts) {
1331
- const bytes = fromString2(message, "base64url");
1359
+ let bytes;
1360
+ try {
1361
+ bytes = fromString2(message, "base64url");
1362
+ } catch (err) {
1363
+ throw new HpkeError(
1364
+ "ERR_MALFORMED_MESSAGE",
1365
+ "malformed message: not base64url",
1366
+ { cause: err }
1367
+ );
1368
+ }
1332
1369
  const plaintext = await decryptBytes(
1333
1370
  keypair,
1334
1371
  bytes,
@@ -1339,7 +1376,8 @@ async function decryptFromString(keypair, message, opts) {
1339
1376
  __name(decryptFromString, "decryptFromString");
1340
1377
  function validateKeysize(keysize) {
1341
1378
  if (keysize !== 128 && keysize !== 256) {
1342
- throw new Error(
1379
+ throw new HpkeError(
1380
+ "ERR_INVALID_KEYSIZE",
1343
1381
  `invalid keysize: ${keysize} (expected 128 or 256)`
1344
1382
  );
1345
1383
  }
@@ -1347,7 +1385,8 @@ function validateKeysize(keysize) {
1347
1385
  __name(validateKeysize, "validateKeysize");
1348
1386
  function validateRawKeyBytes(raw) {
1349
1387
  if (raw.length !== 16 && raw.length !== 32) {
1350
- throw new Error(
1388
+ throw new HpkeError(
1389
+ "ERR_INVALID_AES_KEY",
1351
1390
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1352
1391
  );
1353
1392
  }
@@ -1361,19 +1400,20 @@ function normalizeInfo(info) {
1361
1400
  __name(normalizeInfo, "normalizeInfo");
1362
1401
  async function exportAesKeyBytes(key) {
1363
1402
  if (!key.extractable) {
1364
- throw new Error(
1403
+ throw new HpkeError(
1404
+ "ERR_INVALID_AES_KEY",
1365
1405
  "aesKey must be extractable: its raw bytes are what get sealed"
1366
1406
  );
1367
1407
  }
1368
1408
  return new Uint8Array(await subtle.exportKey("raw", key));
1369
1409
  }
1370
1410
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1371
- async function importAesKey(raw) {
1411
+ async function importAesKey(raw, extractable) {
1372
1412
  return subtle.importKey(
1373
1413
  "raw",
1374
1414
  raw,
1375
1415
  { name: "AES-GCM" },
1376
- true,
1416
+ extractable,
1377
1417
  ["encrypt", "decrypt"]
1378
1418
  );
1379
1419
  }
@@ -1428,7 +1468,10 @@ async function keySchedule(sharedSecret, info) {
1428
1468
  __name(keySchedule, "keySchedule");
1429
1469
  async function openRawBytes(keypair, wrapped, opts) {
1430
1470
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1431
- throw new Error("malformed envelope: too short");
1471
+ throw new HpkeError(
1472
+ "ERR_MALFORMED_ENVELOPE",
1473
+ "malformed envelope: too short"
1474
+ );
1432
1475
  }
1433
1476
  const info = normalizeInfo(opts?.info);
1434
1477
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1440,7 +1483,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1440
1483
  __name(openRawBytes, "openRawBytes");
1441
1484
  async function openBytes(keypair, wrapped, opts) {
1442
1485
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1443
- return importAesKey(keyBytes);
1486
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1444
1487
  }
1445
1488
  __name(openBytes, "openBytes");
1446
1489
  async function encryptKey(recipient, aesKey, opts) {
@@ -1450,6 +1493,12 @@ async function encryptKey(recipient, aesKey, opts) {
1450
1493
  validateRawKeyBytes(aesKey);
1451
1494
  keyBytes = aesKey;
1452
1495
  } else if (aesKey) {
1496
+ if (aesKey.algorithm.name !== "AES-GCM") {
1497
+ throw new HpkeError(
1498
+ "ERR_INVALID_AES_KEY",
1499
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1500
+ );
1501
+ }
1453
1502
  keyBytes = await exportAesKeyBytes(aesKey);
1454
1503
  validateRawKeyBytes(keyBytes);
1455
1504
  } else {
@@ -1464,8 +1513,12 @@ async function encryptKey(recipient, aesKey, opts) {
1464
1513
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1465
1514
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1466
1515
  const wrapped = concat2(enc, ciphertext);
1467
- const aesGcmKey = await importAesKey(keyBytes);
1468
- return { enc: wrapped, key: aesGcmKey };
1516
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1517
+ const aesGcmKey = await importAesKey(
1518
+ keyBytes,
1519
+ opts?.extractable ?? false
1520
+ );
1521
+ return { wrapped, key: aesGcmKey };
1469
1522
  }
1470
1523
  __name(encryptKey, "encryptKey");
1471
1524
 
@@ -1495,24 +1548,44 @@ async function aeadOpen(key, nonce, ciphertext) {
1495
1548
  false,
1496
1549
  ["decrypt"]
1497
1550
  );
1498
- const pt = await subtle2.decrypt(
1499
- { name: "AES-GCM", iv: nonce },
1500
- k,
1501
- ciphertext
1502
- );
1551
+ let pt;
1552
+ try {
1553
+ pt = await subtle2.decrypt(
1554
+ { name: "AES-GCM", iv: nonce },
1555
+ k,
1556
+ ciphertext
1557
+ );
1558
+ } catch (err) {
1559
+ throw new HpkeError(
1560
+ "ERR_DECRYPT_FAILED",
1561
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1562
+ { cause: err }
1563
+ );
1564
+ }
1503
1565
  return new Uint8Array(pt);
1504
1566
  }
1505
1567
  __name(aeadOpen, "aeadOpen");
1506
1568
  async function encryptBytes(recipient, message, aesKey, opts) {
1569
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1570
+ throw new HpkeError(
1571
+ "ERR_INVALID_AES_KEY",
1572
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1573
+ );
1574
+ }
1507
1575
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1508
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1576
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1509
1577
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1510
1578
  const ct = new Uint8Array(await subtle2.encrypt(
1511
1579
  { name: "AES-GCM", iv },
1512
1580
  key,
1513
1581
  plaintext
1514
1582
  ));
1515
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1583
+ return concat2(
1584
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1585
+ wrapped,
1586
+ iv,
1587
+ ct
1588
+ );
1516
1589
  }
1517
1590
  __name(encryptBytes, "encryptBytes");
1518
1591
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1578,7 +1651,8 @@ async function exportRawPublic(key) {
1578
1651
  __name(exportRawPublic, "exportRawPublic");
1579
1652
  async function importRawPublic(raw) {
1580
1653
  if (raw.length !== ENC_LENGTH) {
1581
- throw new Error(
1654
+ throw new HpkeError(
1655
+ "ERR_INVALID_RECIPIENT_KEY",
1582
1656
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1583
1657
  );
1584
1658
  }
@@ -1596,44 +1670,83 @@ async function resolveRecipientPublicKey(recipient) {
1596
1670
  return importRawPublic(recipient);
1597
1671
  }
1598
1672
  if (recipient instanceof CryptoKey) {
1599
- if (recipient.type !== "public") {
1600
- throw new Error("recipient CryptoKey must be a public key");
1601
- }
1602
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1603
- return recipient;
1673
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1604
1674
  }
1605
1675
  if (typeof recipient.publicKey === "string") {
1606
1676
  const { publicKey, encoding } = recipient;
1607
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1677
+ let decoded;
1678
+ try {
1679
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1680
+ } catch (err) {
1681
+ throw new HpkeError(
1682
+ "ERR_INVALID_RECIPIENT_KEY",
1683
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1684
+ { cause: err }
1685
+ );
1686
+ }
1687
+ return importRawPublic(decoded);
1608
1688
  }
1609
1689
  const pair = recipient;
1610
1690
  if (pair.publicKey instanceof CryptoKey) {
1611
- if (pair.publicKey.type !== "public") {
1612
- throw new Error("recipient keypair publicKey must be a public key");
1613
- }
1614
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1615
- return pair.publicKey;
1691
+ return assertRecipientPublicKey(
1692
+ pair.publicKey,
1693
+ "recipient keypair publicKey"
1694
+ );
1616
1695
  }
1617
- throw new Error("unrecognized recipient key form");
1696
+ throw new HpkeError(
1697
+ "ERR_INVALID_RECIPIENT_KEY",
1698
+ "unrecognized recipient key form"
1699
+ );
1618
1700
  }
1619
1701
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1620
- function assertX25519Algorithm(key, context) {
1702
+ function assertRecipientPublicKey(key, context) {
1703
+ if (key.type !== "public") {
1704
+ throw new HpkeError(
1705
+ "ERR_INVALID_RECIPIENT_KEY",
1706
+ `${context} must be a public key`
1707
+ );
1708
+ }
1709
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1710
+ if (!key.extractable) {
1711
+ throw new HpkeError(
1712
+ "ERR_INVALID_RECIPIENT_KEY",
1713
+ `${context} must be extractable (the KEM exports it)`
1714
+ );
1715
+ }
1716
+ return key;
1717
+ }
1718
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1719
+ function assertX25519Algorithm(key, context, code2) {
1621
1720
  if (key.algorithm.name !== "X25519") {
1622
- throw new Error(
1721
+ throw new HpkeError(
1722
+ code2,
1623
1723
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1624
1724
  );
1625
1725
  }
1626
1726
  }
1627
1727
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1628
1728
  async function dh(priv, pub) {
1629
- const bits = await subtle2.deriveBits(
1630
- { name: "X25519", public: pub },
1631
- priv,
1632
- 256
1633
- );
1729
+ let bits;
1730
+ try {
1731
+ bits = await subtle2.deriveBits(
1732
+ { name: "X25519", public: pub },
1733
+ priv,
1734
+ 256
1735
+ );
1736
+ } catch (err) {
1737
+ if (err?.name === "OperationError") {
1738
+ throw new HpkeError(
1739
+ "ERR_SMALL_ORDER_KEY",
1740
+ "X25519 deriveBits rejected a small-order public key",
1741
+ { cause: err }
1742
+ );
1743
+ }
1744
+ throw err;
1745
+ }
1634
1746
  const bytes = new Uint8Array(bits);
1635
1747
  if (bytes.every((b) => b === 0)) {
1636
- throw new Error(
1748
+ throw new HpkeError(
1749
+ "ERR_SMALL_ORDER_KEY",
1637
1750
  "X25519 shared secret is all-zero (small-order public key?)"
1638
1751
  );
1639
1752
  }
@@ -1672,9 +1785,33 @@ async function encap(pkR) {
1672
1785
  __name(encap, "encap");
1673
1786
  async function decap(enc, keypair) {
1674
1787
  if (keypair.privateKey.type !== "private") {
1675
- throw new Error("keypair.privateKey must be a private key");
1788
+ throw new HpkeError(
1789
+ "ERR_INVALID_KEYPAIR",
1790
+ "keypair.privateKey must be a private key"
1791
+ );
1676
1792
  }
1677
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1793
+ assertX25519Algorithm(
1794
+ keypair.privateKey,
1795
+ "keypair.privateKey",
1796
+ "ERR_INVALID_KEYPAIR"
1797
+ );
1798
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1799
+ throw new HpkeError(
1800
+ "ERR_INVALID_KEYPAIR",
1801
+ "keypair.privateKey must have the deriveBits usage"
1802
+ );
1803
+ }
1804
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1805
+ throw new HpkeError(
1806
+ "ERR_INVALID_KEYPAIR",
1807
+ "keypair.publicKey must be an extractable public key"
1808
+ );
1809
+ }
1810
+ assertX25519Algorithm(
1811
+ keypair.publicKey,
1812
+ "keypair.publicKey",
1813
+ "ERR_INVALID_KEYPAIR"
1814
+ );
1678
1815
  const pkE = await subtle2.importKey(
1679
1816
  "raw",
1680
1817
  enc,
@@ -1733,6 +1870,8 @@ var HPKE_SUITE_ID = concat2(
1733
1870
  i2osp(KDF_ID, 2),
1734
1871
  i2osp(AEAD_ID, 2)
1735
1872
  );
1873
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1874
+ var KEY_ID_NONCE_LENGTH = 12;
1736
1875
  export {
1737
1876
  AEAD_ID,
1738
1877
  AEAD_TAG_LENGTH,
@@ -1742,6 +1881,8 @@ export {
1742
1881
  KDF_ID,
1743
1882
  KEM_ID,
1744
1883
  KEM_SUITE_ID,
1884
+ KEY_ID_LABEL,
1885
+ KEY_ID_NONCE_LENGTH,
1745
1886
  MODE_BASE,
1746
1887
  NK,
1747
1888
  NN,