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.
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/errors.ts
22
+ var errors_exports = {};
23
+ __export(errors_exports, {
24
+ HpkeError: () => HpkeError
25
+ });
26
+ module.exports = __toCommonJS(errors_exports);
27
+ var HpkeError = class extends Error {
28
+ static {
29
+ __name(this, "HpkeError");
30
+ }
31
+ code;
32
+ constructor(code, message, opts) {
33
+ super(message, opts);
34
+ this.name = "HpkeError";
35
+ this.code = code;
36
+ }
37
+ };
38
+ //# sourceMappingURL=errors.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts"],
4
+ "sourcesContent": ["/**\n * The set of stable error codes this package raises.\n *\n * Branch on `err.code`, not on `err.message`: the codes are part of the\n * public API, the prose is not.\n */\nexport type HpkeErrorCode =\n 'ERR_INVALID_RECIPIENT_KEY'|\n 'ERR_INVALID_KEYPAIR'|\n 'ERR_SMALL_ORDER_KEY'|\n 'ERR_MALFORMED_ENVELOPE'|\n 'ERR_MALFORMED_MESSAGE'|\n 'ERR_INVALID_KEYSIZE'|\n 'ERR_INVALID_AES_KEY'|\n 'ERR_DECRYPT_FAILED'\n\n/**\n * An `Error` carrying a stable `code`. Every throw site in this package\n * raises one of these, so callers can branch on the failure instead of\n * pattern matching on a message.\n *\n * Where a WebCrypto failure is being wrapped, the original is kept on\n * `cause`.\n */\nexport class HpkeError extends Error {\n readonly code:HpkeErrorCode\n\n constructor (\n code:HpkeErrorCode,\n message:string,\n opts?:{ cause?:unknown }\n ) {\n super(message, opts)\n this.name = 'HpkeError'\n this.code = code\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBO,IAAM,YAAN,cAAwB,MAAM;AAAA,EAxBrC,OAwBqC;AAAA;AAAA;AAAA,EACxB;AAAA,EAET,YACI,MACA,SACA,MACF;AACE,UAAM,SAAS,IAAI;AACnB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EAChB;AACJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The set of stable error codes this package raises.
3
+ *
4
+ * Branch on `err.code`, not on `err.message`: the codes are part of the
5
+ * public API, the prose is not.
6
+ */
7
+ export type HpkeErrorCode = 'ERR_INVALID_RECIPIENT_KEY' | 'ERR_INVALID_KEYPAIR' | 'ERR_SMALL_ORDER_KEY' | 'ERR_MALFORMED_ENVELOPE' | 'ERR_MALFORMED_MESSAGE' | 'ERR_INVALID_KEYSIZE' | 'ERR_INVALID_AES_KEY' | 'ERR_DECRYPT_FAILED';
8
+ /**
9
+ * An `Error` carrying a stable `code`. Every throw site in this package
10
+ * raises one of these, so callers can branch on the failure instead of
11
+ * pattern matching on a message.
12
+ *
13
+ * Where a WebCrypto failure is being wrapped, the original is kept on
14
+ * `cause`.
15
+ */
16
+ export declare class HpkeError extends Error {
17
+ readonly code: HpkeErrorCode;
18
+ constructor(code: HpkeErrorCode, message: string, opts?: {
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB,2BAA2B,GAC3B,qBAAqB,GACrB,qBAAqB,GACrB,wBAAwB,GACxB,uBAAuB,GACvB,qBAAqB,GACrB,qBAAqB,GACrB,oBAAoB,CAAA;AAExB;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAChC,QAAQ,CAAC,IAAI,EAAC,aAAa,CAAA;gBAGvB,IAAI,EAAC,aAAa,EAClB,OAAO,EAAC,MAAM,EACd,IAAI,CAAC,EAAC;QAAE,KAAK,CAAC,EAAC,OAAO,CAAA;KAAE;CAM/B"}
package/dist/errors.js ADDED
@@ -0,0 +1,19 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/errors.ts
5
+ var HpkeError = class extends Error {
6
+ static {
7
+ __name(this, "HpkeError");
8
+ }
9
+ code;
10
+ constructor(code, message, opts) {
11
+ super(message, opts);
12
+ this.name = "HpkeError";
13
+ this.code = code;
14
+ }
15
+ };
16
+ export {
17
+ HpkeError
18
+ };
19
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts"],
4
+ "sourcesContent": ["/**\n * The set of stable error codes this package raises.\n *\n * Branch on `err.code`, not on `err.message`: the codes are part of the\n * public API, the prose is not.\n */\nexport type HpkeErrorCode =\n 'ERR_INVALID_RECIPIENT_KEY'|\n 'ERR_INVALID_KEYPAIR'|\n 'ERR_SMALL_ORDER_KEY'|\n 'ERR_MALFORMED_ENVELOPE'|\n 'ERR_MALFORMED_MESSAGE'|\n 'ERR_INVALID_KEYSIZE'|\n 'ERR_INVALID_AES_KEY'|\n 'ERR_DECRYPT_FAILED'\n\n/**\n * An `Error` carrying a stable `code`. Every throw site in this package\n * raises one of these, so callers can branch on the failure instead of\n * pattern matching on a message.\n *\n * Where a WebCrypto failure is being wrapped, the original is kept on\n * `cause`.\n */\nexport class HpkeError extends Error {\n readonly code:HpkeErrorCode\n\n constructor (\n code:HpkeErrorCode,\n message:string,\n opts?:{ cause?:unknown }\n ) {\n super(message, opts)\n this.name = 'HpkeError'\n this.code = code\n }\n}\n"],
5
+ "mappings": ";;;;AAwBO,IAAM,YAAN,cAAwB,MAAM;AAAA,EAxBrC,OAwBqC;AAAA;AAAA;AAAA,EACxB;AAAA,EAET,YACI,MACA,SACA,MACF;AACE,UAAM,SAAS,IAAI;AACnB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EAChB;AACJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";var o=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var s=Object.getOwnPropertyNames;var t=Object.prototype.hasOwnProperty;var A=(R,E)=>o(R,"name",{value:E,configurable:!0});var D=(R,E)=>{for(var r in E)o(R,r,{get:E[r],enumerable:!0})},n=(R,E,r,e)=>{if(E&&typeof E=="object"||typeof E=="function")for(let _ of s(E))!t.call(R,_)&&_!==r&&o(R,_,{get:()=>E[_],enumerable:!(e=L(E,_))||e.enumerable});return R};var d=R=>n(o({},"__esModule",{value:!0}),R);var p={};D(p,{HpkeError:()=>I});module.exports=d(p);var I=class extends Error{static{A(this,"HpkeError")}code;constructor(E,r,e){super(r,e),this.name="HpkeError",this.code=E}};
2
+ //# sourceMappingURL=errors.min.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts"],
4
+ "sourcesContent": ["/**\n * The set of stable error codes this package raises.\n *\n * Branch on `err.code`, not on `err.message`: the codes are part of the\n * public API, the prose is not.\n */\nexport type HpkeErrorCode =\n 'ERR_INVALID_RECIPIENT_KEY'|\n 'ERR_INVALID_KEYPAIR'|\n 'ERR_SMALL_ORDER_KEY'|\n 'ERR_MALFORMED_ENVELOPE'|\n 'ERR_MALFORMED_MESSAGE'|\n 'ERR_INVALID_KEYSIZE'|\n 'ERR_INVALID_AES_KEY'|\n 'ERR_DECRYPT_FAILED'\n\n/**\n * An `Error` carrying a stable `code`. Every throw site in this package\n * raises one of these, so callers can branch on the failure instead of\n * pattern matching on a message.\n *\n * Where a WebCrypto failure is being wrapped, the original is kept on\n * `cause`.\n */\nexport class HpkeError extends Error {\n readonly code:HpkeErrorCode\n\n constructor (\n code:HpkeErrorCode,\n message:string,\n opts?:{ cause?:unknown }\n ) {\n super(message, opts)\n this.name = 'HpkeError'\n this.code = code\n }\n}\n"],
5
+ "mappings": "4dAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,eAAAE,IAAA,eAAAC,EAAAH,GAwBO,IAAMI,EAAN,cAAwB,KAAM,CAxBrC,MAwBqC,CAAAC,EAAA,kBACxB,KAET,YACIC,EACAC,EACAC,EACF,CACE,MAAMD,EAASC,CAAI,EACnB,KAAK,KAAO,YACZ,KAAK,KAAOF,CAChB,CACJ",
6
+ "names": ["errors_exports", "__export", "HpkeError", "__toCommonJS", "HpkeError", "__name", "code", "message", "opts"]
7
+ }
@@ -0,0 +1,2 @@
1
+ var I=Object.defineProperty;var r=(R,E)=>I(R,"name",{value:E,configurable:!0});var _=class extends Error{static{r(this,"HpkeError")}code;constructor(E,e,o){super(e,o),this.name="HpkeError",this.code=E}};export{_ as HpkeError};
2
+ //# sourceMappingURL=errors.min.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts"],
4
+ "sourcesContent": ["/**\n * The set of stable error codes this package raises.\n *\n * Branch on `err.code`, not on `err.message`: the codes are part of the\n * public API, the prose is not.\n */\nexport type HpkeErrorCode =\n 'ERR_INVALID_RECIPIENT_KEY'|\n 'ERR_INVALID_KEYPAIR'|\n 'ERR_SMALL_ORDER_KEY'|\n 'ERR_MALFORMED_ENVELOPE'|\n 'ERR_MALFORMED_MESSAGE'|\n 'ERR_INVALID_KEYSIZE'|\n 'ERR_INVALID_AES_KEY'|\n 'ERR_DECRYPT_FAILED'\n\n/**\n * An `Error` carrying a stable `code`. Every throw site in this package\n * raises one of these, so callers can branch on the failure instead of\n * pattern matching on a message.\n *\n * Where a WebCrypto failure is being wrapped, the original is kept on\n * `cause`.\n */\nexport class HpkeError extends Error {\n readonly code:HpkeErrorCode\n\n constructor (\n code:HpkeErrorCode,\n message:string,\n opts?:{ cause?:unknown }\n ) {\n super(message, opts)\n this.name = 'HpkeError'\n this.code = code\n }\n}\n"],
5
+ "mappings": "+EAwBO,IAAMA,EAAN,cAAwB,KAAM,CAxBrC,MAwBqC,CAAAC,EAAA,kBACxB,KAET,YACIC,EACAC,EACAC,EACF,CACE,MAAMD,EAASC,CAAI,EACnB,KAAK,KAAO,YACZ,KAAK,KAAOF,CAChB,CACJ",
6
+ "names": ["HpkeError", "__name", "code", "message", "opts"]
7
+ }
package/dist/index.cjs CHANGED
@@ -21,11 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ HpkeError: () => HpkeError,
24
25
  create: () => create2,
25
26
  decrypt: () => decrypt,
26
27
  encrypt: () => encrypt,
27
28
  encryptKey: () => encryptKey,
28
- open: () => open
29
+ keyId: () => keyId,
30
+ open: () => open,
31
+ verifyKeyId: () => verifyKeyId
29
32
  });
30
33
  module.exports = __toCommonJS(index_exports);
31
34
 
@@ -1307,6 +1310,19 @@ function toString2(array, encoding = "utf8") {
1307
1310
  }
1308
1311
  __name(toString2, "toString");
1309
1312
 
1313
+ // src/errors.ts
1314
+ var HpkeError = class extends Error {
1315
+ static {
1316
+ __name(this, "HpkeError");
1317
+ }
1318
+ code;
1319
+ constructor(code2, message, opts) {
1320
+ super(message, opts);
1321
+ this.name = "HpkeError";
1322
+ this.code = code2;
1323
+ }
1324
+ };
1325
+
1310
1326
  // src/constants.ts
1311
1327
  var KEM_ID = 32;
1312
1328
  var KDF_ID = 1;
@@ -1329,6 +1345,8 @@ var HPKE_SUITE_ID = concat2(
1329
1345
  i2osp(KDF_ID, 2),
1330
1346
  i2osp(AEAD_ID, 2)
1331
1347
  );
1348
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1349
+ var KEY_ID_NONCE_LENGTH = 12;
1332
1350
 
1333
1351
  // src/util.ts
1334
1352
  var subtle = globalThis.crypto.subtle;
@@ -1356,24 +1374,44 @@ async function aeadOpen(key, nonce, ciphertext) {
1356
1374
  false,
1357
1375
  ["decrypt"]
1358
1376
  );
1359
- const pt = await subtle.decrypt(
1360
- { name: "AES-GCM", iv: nonce },
1361
- k,
1362
- ciphertext
1363
- );
1377
+ let pt;
1378
+ try {
1379
+ pt = await subtle.decrypt(
1380
+ { name: "AES-GCM", iv: nonce },
1381
+ k,
1382
+ ciphertext
1383
+ );
1384
+ } catch (err) {
1385
+ throw new HpkeError(
1386
+ "ERR_DECRYPT_FAILED",
1387
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1388
+ { cause: err }
1389
+ );
1390
+ }
1364
1391
  return new Uint8Array(pt);
1365
1392
  }
1366
1393
  __name(aeadOpen, "aeadOpen");
1367
1394
  async function encryptBytes(recipient, message, aesKey, opts) {
1395
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1396
+ throw new HpkeError(
1397
+ "ERR_INVALID_AES_KEY",
1398
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1399
+ );
1400
+ }
1368
1401
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1369
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1402
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1370
1403
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1371
1404
  const ct = new Uint8Array(await subtle.encrypt(
1372
1405
  { name: "AES-GCM", iv },
1373
1406
  key,
1374
1407
  plaintext
1375
1408
  ));
1376
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1409
+ return concat2(
1410
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1411
+ wrapped,
1412
+ iv,
1413
+ ct
1414
+ );
1377
1415
  }
1378
1416
  __name(encryptBytes, "encryptBytes");
1379
1417
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1439,7 +1477,8 @@ async function exportRawPublic(key) {
1439
1477
  __name(exportRawPublic, "exportRawPublic");
1440
1478
  async function importRawPublic(raw) {
1441
1479
  if (raw.length !== ENC_LENGTH) {
1442
- throw new Error(
1480
+ throw new HpkeError(
1481
+ "ERR_INVALID_RECIPIENT_KEY",
1443
1482
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1444
1483
  );
1445
1484
  }
@@ -1457,44 +1496,83 @@ async function resolveRecipientPublicKey(recipient) {
1457
1496
  return importRawPublic(recipient);
1458
1497
  }
1459
1498
  if (recipient instanceof CryptoKey) {
1460
- if (recipient.type !== "public") {
1461
- throw new Error("recipient CryptoKey must be a public key");
1462
- }
1463
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1464
- return recipient;
1499
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1465
1500
  }
1466
1501
  if (typeof recipient.publicKey === "string") {
1467
1502
  const { publicKey, encoding } = recipient;
1468
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1503
+ let decoded;
1504
+ try {
1505
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1506
+ } catch (err) {
1507
+ throw new HpkeError(
1508
+ "ERR_INVALID_RECIPIENT_KEY",
1509
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1510
+ { cause: err }
1511
+ );
1512
+ }
1513
+ return importRawPublic(decoded);
1469
1514
  }
1470
1515
  const pair = recipient;
1471
1516
  if (pair.publicKey instanceof CryptoKey) {
1472
- if (pair.publicKey.type !== "public") {
1473
- throw new Error("recipient keypair publicKey must be a public key");
1474
- }
1475
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1476
- return pair.publicKey;
1517
+ return assertRecipientPublicKey(
1518
+ pair.publicKey,
1519
+ "recipient keypair publicKey"
1520
+ );
1477
1521
  }
1478
- throw new Error("unrecognized recipient key form");
1522
+ throw new HpkeError(
1523
+ "ERR_INVALID_RECIPIENT_KEY",
1524
+ "unrecognized recipient key form"
1525
+ );
1479
1526
  }
1480
1527
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1481
- function assertX25519Algorithm(key, context) {
1528
+ function assertRecipientPublicKey(key, context) {
1529
+ if (key.type !== "public") {
1530
+ throw new HpkeError(
1531
+ "ERR_INVALID_RECIPIENT_KEY",
1532
+ `${context} must be a public key`
1533
+ );
1534
+ }
1535
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1536
+ if (!key.extractable) {
1537
+ throw new HpkeError(
1538
+ "ERR_INVALID_RECIPIENT_KEY",
1539
+ `${context} must be extractable (the KEM exports it)`
1540
+ );
1541
+ }
1542
+ return key;
1543
+ }
1544
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1545
+ function assertX25519Algorithm(key, context, code2) {
1482
1546
  if (key.algorithm.name !== "X25519") {
1483
- throw new Error(
1547
+ throw new HpkeError(
1548
+ code2,
1484
1549
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1485
1550
  );
1486
1551
  }
1487
1552
  }
1488
1553
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1489
1554
  async function dh(priv, pub) {
1490
- const bits = await subtle.deriveBits(
1491
- { name: "X25519", public: pub },
1492
- priv,
1493
- 256
1494
- );
1555
+ let bits;
1556
+ try {
1557
+ bits = await subtle.deriveBits(
1558
+ { name: "X25519", public: pub },
1559
+ priv,
1560
+ 256
1561
+ );
1562
+ } catch (err) {
1563
+ if (err?.name === "OperationError") {
1564
+ throw new HpkeError(
1565
+ "ERR_SMALL_ORDER_KEY",
1566
+ "X25519 deriveBits rejected a small-order public key",
1567
+ { cause: err }
1568
+ );
1569
+ }
1570
+ throw err;
1571
+ }
1495
1572
  const bytes = new Uint8Array(bits);
1496
1573
  if (bytes.every((b) => b === 0)) {
1497
- throw new Error(
1574
+ throw new HpkeError(
1575
+ "ERR_SMALL_ORDER_KEY",
1498
1576
  "X25519 shared secret is all-zero (small-order public key?)"
1499
1577
  );
1500
1578
  }
@@ -1533,9 +1611,33 @@ async function encap(pkR) {
1533
1611
  __name(encap, "encap");
1534
1612
  async function decap(enc, keypair) {
1535
1613
  if (keypair.privateKey.type !== "private") {
1536
- throw new Error("keypair.privateKey must be a private key");
1614
+ throw new HpkeError(
1615
+ "ERR_INVALID_KEYPAIR",
1616
+ "keypair.privateKey must be a private key"
1617
+ );
1537
1618
  }
1538
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1619
+ assertX25519Algorithm(
1620
+ keypair.privateKey,
1621
+ "keypair.privateKey",
1622
+ "ERR_INVALID_KEYPAIR"
1623
+ );
1624
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1625
+ throw new HpkeError(
1626
+ "ERR_INVALID_KEYPAIR",
1627
+ "keypair.privateKey must have the deriveBits usage"
1628
+ );
1629
+ }
1630
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1631
+ throw new HpkeError(
1632
+ "ERR_INVALID_KEYPAIR",
1633
+ "keypair.publicKey must be an extractable public key"
1634
+ );
1635
+ }
1636
+ assertX25519Algorithm(
1637
+ keypair.publicKey,
1638
+ "keypair.publicKey",
1639
+ "ERR_INVALID_KEYPAIR"
1640
+ );
1539
1641
  const pkE = await subtle.importKey(
1540
1642
  "raw",
1541
1643
  enc,
@@ -1549,6 +1651,45 @@ async function decap(enc, keypair) {
1549
1651
  return extractAndExpand(dhBytes, kemContext);
1550
1652
  }
1551
1653
  __name(decap, "decap");
1654
+ async function keyId(key) {
1655
+ if (key.algorithm.name !== "AES-GCM") {
1656
+ throw new HpkeError(
1657
+ "ERR_INVALID_AES_KEY",
1658
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1659
+ );
1660
+ }
1661
+ if (!key.usages.includes("encrypt")) {
1662
+ throw new HpkeError(
1663
+ "ERR_INVALID_AES_KEY",
1664
+ "keyId needs a key with the 'encrypt' usage"
1665
+ );
1666
+ }
1667
+ const labelHash = new Uint8Array(
1668
+ await subtle.digest("SHA-256", KEY_ID_LABEL)
1669
+ );
1670
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1671
+ const ciphertext = new Uint8Array(await subtle.encrypt(
1672
+ { name: "AES-GCM", iv: nonce },
1673
+ key,
1674
+ KEY_ID_LABEL
1675
+ ));
1676
+ const digest2 = await subtle.digest(
1677
+ "SHA-256",
1678
+ concat2(KEY_ID_LABEL, ciphertext)
1679
+ );
1680
+ return toString2(new Uint8Array(digest2), "base64url");
1681
+ }
1682
+ __name(keyId, "keyId");
1683
+ async function verifyKeyId(key, expected) {
1684
+ const actual = await keyId(key);
1685
+ if (actual.length !== expected.length) return false;
1686
+ let diff = 0;
1687
+ for (let i = 0; i < actual.length; i++) {
1688
+ diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
1689
+ }
1690
+ return diff === 0;
1691
+ }
1692
+ __name(verifyKeyId, "verifyKeyId");
1552
1693
  function i2osp(n, len) {
1553
1694
  const out = new Uint8Array(len);
1554
1695
  let v = n;
@@ -1575,8 +1716,8 @@ __name(concat2, "concat");
1575
1716
  // src/index.ts
1576
1717
  var subtle2 = globalThis.crypto.subtle;
1577
1718
  async function create2(recipient, opts) {
1578
- const { enc, key } = await encryptKey(recipient, null, opts);
1579
- return { enc, key };
1719
+ const { wrapped, key } = await encryptKey(recipient, null, opts);
1720
+ return { wrapped, key };
1580
1721
  }
1581
1722
  __name(create2, "create");
1582
1723
  var open = Object.assign(openBytes, {
@@ -1591,13 +1732,19 @@ var decrypt = Object.assign(decryptBytes, {
1591
1732
  });
1592
1733
  async function decryptBytes(keypair, message, opts) {
1593
1734
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1594
- throw new Error("malformed message: too short");
1735
+ throw new HpkeError(
1736
+ "ERR_MALFORMED_MESSAGE",
1737
+ "malformed message: too short"
1738
+ );
1595
1739
  }
1596
1740
  const wrappedLen = message[0] << 8 | message[1];
1597
1741
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1598
1742
  const ctStart = ivStart + NN;
1599
1743
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1600
- throw new Error("malformed message: too short");
1744
+ throw new HpkeError(
1745
+ "ERR_MALFORMED_MESSAGE",
1746
+ "malformed message: too short"
1747
+ );
1601
1748
  }
1602
1749
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1603
1750
  const iv = message.slice(ivStart, ctStart);
@@ -1608,11 +1755,20 @@ async function decryptBytes(keypair, message, opts) {
1608
1755
  opts?.info !== void 0 ? { info: opts.info } : void 0
1609
1756
  );
1610
1757
  const key = await importAesKeyDecryptOnly(keyBytes);
1611
- const pt = await subtle2.decrypt(
1612
- { name: "AES-GCM", iv },
1613
- key,
1614
- ciphertext
1615
- );
1758
+ let pt;
1759
+ try {
1760
+ pt = await subtle2.decrypt(
1761
+ { name: "AES-GCM", iv },
1762
+ key,
1763
+ ciphertext
1764
+ );
1765
+ } catch (err) {
1766
+ throw new HpkeError(
1767
+ "ERR_DECRYPT_FAILED",
1768
+ "failed to decrypt the message: tampered ciphertext",
1769
+ { cause: err }
1770
+ );
1771
+ }
1616
1772
  return new Uint8Array(pt);
1617
1773
  }
1618
1774
  __name(decryptBytes, "decryptBytes");
@@ -1622,7 +1778,16 @@ async function decryptToString(keypair, message, opts) {
1622
1778
  }
1623
1779
  __name(decryptToString, "decryptToString");
1624
1780
  async function decryptFromString(keypair, message, opts) {
1625
- const bytes = fromString2(message, "base64url");
1781
+ let bytes;
1782
+ try {
1783
+ bytes = fromString2(message, "base64url");
1784
+ } catch (err) {
1785
+ throw new HpkeError(
1786
+ "ERR_MALFORMED_MESSAGE",
1787
+ "malformed message: not base64url",
1788
+ { cause: err }
1789
+ );
1790
+ }
1626
1791
  const plaintext = await decryptBytes(
1627
1792
  keypair,
1628
1793
  bytes,
@@ -1633,7 +1798,8 @@ async function decryptFromString(keypair, message, opts) {
1633
1798
  __name(decryptFromString, "decryptFromString");
1634
1799
  function validateKeysize(keysize) {
1635
1800
  if (keysize !== 128 && keysize !== 256) {
1636
- throw new Error(
1801
+ throw new HpkeError(
1802
+ "ERR_INVALID_KEYSIZE",
1637
1803
  `invalid keysize: ${keysize} (expected 128 or 256)`
1638
1804
  );
1639
1805
  }
@@ -1641,7 +1807,8 @@ function validateKeysize(keysize) {
1641
1807
  __name(validateKeysize, "validateKeysize");
1642
1808
  function validateRawKeyBytes(raw) {
1643
1809
  if (raw.length !== 16 && raw.length !== 32) {
1644
- throw new Error(
1810
+ throw new HpkeError(
1811
+ "ERR_INVALID_AES_KEY",
1645
1812
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1646
1813
  );
1647
1814
  }
@@ -1655,19 +1822,20 @@ function normalizeInfo(info) {
1655
1822
  __name(normalizeInfo, "normalizeInfo");
1656
1823
  async function exportAesKeyBytes(key) {
1657
1824
  if (!key.extractable) {
1658
- throw new Error(
1825
+ throw new HpkeError(
1826
+ "ERR_INVALID_AES_KEY",
1659
1827
  "aesKey must be extractable: its raw bytes are what get sealed"
1660
1828
  );
1661
1829
  }
1662
1830
  return new Uint8Array(await subtle2.exportKey("raw", key));
1663
1831
  }
1664
1832
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1665
- async function importAesKey(raw) {
1833
+ async function importAesKey(raw, extractable) {
1666
1834
  return subtle2.importKey(
1667
1835
  "raw",
1668
1836
  raw,
1669
1837
  { name: "AES-GCM" },
1670
- true,
1838
+ extractable,
1671
1839
  ["encrypt", "decrypt"]
1672
1840
  );
1673
1841
  }
@@ -1722,7 +1890,10 @@ async function keySchedule(sharedSecret, info) {
1722
1890
  __name(keySchedule, "keySchedule");
1723
1891
  async function openRawBytes(keypair, wrapped, opts) {
1724
1892
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1725
- throw new Error("malformed envelope: too short");
1893
+ throw new HpkeError(
1894
+ "ERR_MALFORMED_ENVELOPE",
1895
+ "malformed envelope: too short"
1896
+ );
1726
1897
  }
1727
1898
  const info = normalizeInfo(opts?.info);
1728
1899
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1734,7 +1905,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1734
1905
  __name(openRawBytes, "openRawBytes");
1735
1906
  async function openBytes(keypair, wrapped, opts) {
1736
1907
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1737
- return importAesKey(keyBytes);
1908
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1738
1909
  }
1739
1910
  __name(openBytes, "openBytes");
1740
1911
  async function encryptKey(recipient, aesKey, opts) {
@@ -1744,6 +1915,12 @@ async function encryptKey(recipient, aesKey, opts) {
1744
1915
  validateRawKeyBytes(aesKey);
1745
1916
  keyBytes = aesKey;
1746
1917
  } else if (aesKey) {
1918
+ if (aesKey.algorithm.name !== "AES-GCM") {
1919
+ throw new HpkeError(
1920
+ "ERR_INVALID_AES_KEY",
1921
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1922
+ );
1923
+ }
1747
1924
  keyBytes = await exportAesKeyBytes(aesKey);
1748
1925
  validateRawKeyBytes(keyBytes);
1749
1926
  } else {
@@ -1758,8 +1935,12 @@ async function encryptKey(recipient, aesKey, opts) {
1758
1935
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1759
1936
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1760
1937
  const wrapped = concat2(enc, ciphertext);
1761
- const aesGcmKey = await importAesKey(keyBytes);
1762
- return { enc: wrapped, key: aesGcmKey };
1938
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1939
+ const aesGcmKey = await importAesKey(
1940
+ keyBytes,
1941
+ opts?.extractable ?? false
1942
+ );
1943
+ return { wrapped, key: aesGcmKey };
1763
1944
  }
1764
1945
  __name(encryptKey, "encryptKey");
1765
1946
  //# sourceMappingURL=index.cjs.map