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.
@@ -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,13 @@ 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
- open: () => open,
28
- seal: () => seal
28
+ encryptKey: () => encryptKey,
29
+ keyId: () => keyId,
30
+ open: () => open
29
31
  });
30
32
  module.exports = __toCommonJS(index_exports);
31
33
 
@@ -1307,6 +1309,19 @@ function toString2(array, encoding = "utf8") {
1307
1309
  }
1308
1310
  __name(toString2, "toString");
1309
1311
 
1312
+ // src/errors.ts
1313
+ var HpkeError = class extends Error {
1314
+ static {
1315
+ __name(this, "HpkeError");
1316
+ }
1317
+ code;
1318
+ constructor(code2, message, opts) {
1319
+ super(message, opts);
1320
+ this.name = "HpkeError";
1321
+ this.code = code2;
1322
+ }
1323
+ };
1324
+
1310
1325
  // src/constants.ts
1311
1326
  var KEM_ID = 32;
1312
1327
  var KDF_ID = 1;
@@ -1329,6 +1344,8 @@ var HPKE_SUITE_ID = concat2(
1329
1344
  i2osp(KDF_ID, 2),
1330
1345
  i2osp(AEAD_ID, 2)
1331
1346
  );
1347
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1348
+ var KEY_ID_NONCE_LENGTH = 12;
1332
1349
 
1333
1350
  // src/util.ts
1334
1351
  var subtle = globalThis.crypto.subtle;
@@ -1356,34 +1373,38 @@ async function aeadOpen(key, nonce, ciphertext) {
1356
1373
  false,
1357
1374
  ["decrypt"]
1358
1375
  );
1359
- const pt = await subtle.decrypt(
1360
- { name: "AES-GCM", iv: nonce },
1361
- k,
1362
- ciphertext
1363
- );
1376
+ let pt;
1377
+ try {
1378
+ pt = await subtle.decrypt(
1379
+ { name: "AES-GCM", iv: nonce },
1380
+ k,
1381
+ ciphertext
1382
+ );
1383
+ } catch (err) {
1384
+ throw new HpkeError(
1385
+ "ERR_DECRYPT_FAILED",
1386
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1387
+ { cause: err }
1388
+ );
1389
+ }
1364
1390
  return new Uint8Array(pt);
1365
1391
  }
1366
1392
  __name(aeadOpen, "aeadOpen");
1367
1393
  async function encryptBytes(recipient, message, aesKey, opts) {
1368
1394
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1369
- let enc;
1370
- let key;
1371
- if (!aesKey) {
1372
- const keys = await create2(recipient, opts);
1373
- enc = keys.enc;
1374
- key = keys.key;
1375
- } else {
1376
- const keys = await seal(recipient, aesKey, opts);
1377
- enc = keys.enc;
1378
- key = keys.key;
1379
- }
1395
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1380
1396
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1381
1397
  const ct = new Uint8Array(await subtle.encrypt(
1382
1398
  { name: "AES-GCM", iv },
1383
1399
  key,
1384
1400
  plaintext
1385
1401
  ));
1386
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1402
+ return concat2(
1403
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1404
+ wrapped,
1405
+ iv,
1406
+ ct
1407
+ );
1387
1408
  }
1388
1409
  __name(encryptBytes, "encryptBytes");
1389
1410
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1449,7 +1470,8 @@ async function exportRawPublic(key) {
1449
1470
  __name(exportRawPublic, "exportRawPublic");
1450
1471
  async function importRawPublic(raw) {
1451
1472
  if (raw.length !== ENC_LENGTH) {
1452
- throw new Error(
1473
+ throw new HpkeError(
1474
+ "ERR_INVALID_RECIPIENT_KEY",
1453
1475
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1454
1476
  );
1455
1477
  }
@@ -1468,9 +1490,16 @@ async function resolveRecipientPublicKey(recipient) {
1468
1490
  }
1469
1491
  if (recipient instanceof CryptoKey) {
1470
1492
  if (recipient.type !== "public") {
1471
- throw new Error("recipient CryptoKey must be a public key");
1493
+ throw new HpkeError(
1494
+ "ERR_INVALID_RECIPIENT_KEY",
1495
+ "recipient CryptoKey must be a public key"
1496
+ );
1472
1497
  }
1473
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1498
+ assertX25519Algorithm(
1499
+ recipient,
1500
+ "recipient CryptoKey",
1501
+ "ERR_INVALID_RECIPIENT_KEY"
1502
+ );
1474
1503
  return recipient;
1475
1504
  }
1476
1505
  if (typeof recipient.publicKey === "string") {
@@ -1480,17 +1509,28 @@ async function resolveRecipientPublicKey(recipient) {
1480
1509
  const pair = recipient;
1481
1510
  if (pair.publicKey instanceof CryptoKey) {
1482
1511
  if (pair.publicKey.type !== "public") {
1483
- throw new Error("recipient keypair publicKey must be a public key");
1512
+ throw new HpkeError(
1513
+ "ERR_INVALID_RECIPIENT_KEY",
1514
+ "recipient keypair publicKey must be a public key"
1515
+ );
1484
1516
  }
1485
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1517
+ assertX25519Algorithm(
1518
+ pair.publicKey,
1519
+ "recipient keypair publicKey",
1520
+ "ERR_INVALID_RECIPIENT_KEY"
1521
+ );
1486
1522
  return pair.publicKey;
1487
1523
  }
1488
- throw new Error("unrecognized recipient key form");
1524
+ throw new HpkeError(
1525
+ "ERR_INVALID_RECIPIENT_KEY",
1526
+ "unrecognized recipient key form"
1527
+ );
1489
1528
  }
1490
1529
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1491
- function assertX25519Algorithm(key, context) {
1530
+ function assertX25519Algorithm(key, context, code2) {
1492
1531
  if (key.algorithm.name !== "X25519") {
1493
- throw new Error(
1532
+ throw new HpkeError(
1533
+ code2,
1494
1534
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1495
1535
  );
1496
1536
  }
@@ -1504,7 +1544,8 @@ async function dh(priv, pub) {
1504
1544
  );
1505
1545
  const bytes = new Uint8Array(bits);
1506
1546
  if (bytes.every((b) => b === 0)) {
1507
- throw new Error(
1547
+ throw new HpkeError(
1548
+ "ERR_SMALL_ORDER_KEY",
1508
1549
  "X25519 shared secret is all-zero (small-order public key?)"
1509
1550
  );
1510
1551
  }
@@ -1543,9 +1584,16 @@ async function encap(pkR) {
1543
1584
  __name(encap, "encap");
1544
1585
  async function decap(enc, keypair) {
1545
1586
  if (keypair.privateKey.type !== "private") {
1546
- throw new Error("keypair.privateKey must be a private key");
1587
+ throw new HpkeError(
1588
+ "ERR_INVALID_KEYPAIR",
1589
+ "keypair.privateKey must be a private key"
1590
+ );
1547
1591
  }
1548
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1592
+ assertX25519Algorithm(
1593
+ keypair.privateKey,
1594
+ "keypair.privateKey",
1595
+ "ERR_INVALID_KEYPAIR"
1596
+ );
1549
1597
  const pkE = await subtle.importKey(
1550
1598
  "raw",
1551
1599
  enc,
@@ -1559,6 +1607,35 @@ async function decap(enc, keypair) {
1559
1607
  return extractAndExpand(dhBytes, kemContext);
1560
1608
  }
1561
1609
  __name(decap, "decap");
1610
+ async function keyId(key) {
1611
+ if (key.algorithm.name !== "AES-GCM") {
1612
+ throw new HpkeError(
1613
+ "ERR_INVALID_AES_KEY",
1614
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1615
+ );
1616
+ }
1617
+ if (!key.usages.includes("encrypt")) {
1618
+ throw new HpkeError(
1619
+ "ERR_INVALID_AES_KEY",
1620
+ "keyId needs a key with the 'encrypt' usage"
1621
+ );
1622
+ }
1623
+ const labelHash = new Uint8Array(
1624
+ await subtle.digest("SHA-256", KEY_ID_LABEL)
1625
+ );
1626
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1627
+ const ciphertext = new Uint8Array(await subtle.encrypt(
1628
+ { name: "AES-GCM", iv: nonce },
1629
+ key,
1630
+ KEY_ID_LABEL
1631
+ ));
1632
+ const digest2 = await subtle.digest(
1633
+ "SHA-256",
1634
+ concat2(KEY_ID_LABEL, ciphertext)
1635
+ );
1636
+ return toString2(new Uint8Array(digest2), "base64url");
1637
+ }
1638
+ __name(keyId, "keyId");
1562
1639
  function i2osp(n, len) {
1563
1640
  const out = new Uint8Array(len);
1564
1641
  let v = n;
@@ -1586,14 +1663,9 @@ __name(concat2, "concat");
1586
1663
  var subtle2 = globalThis.crypto.subtle;
1587
1664
  async function create2(recipient, opts) {
1588
1665
  const { wrapped, key } = await encryptKey(recipient, null, opts);
1589
- return { enc: wrapped, key };
1666
+ return { wrapped, key };
1590
1667
  }
1591
1668
  __name(create2, "create");
1592
- async function seal(recipient, aesKey, opts) {
1593
- const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1594
- return { enc: wrapped, key };
1595
- }
1596
- __name(seal, "seal");
1597
1669
  var open = Object.assign(openBytes, {
1598
1670
  raw: openRawBytes
1599
1671
  });
@@ -1606,13 +1678,19 @@ var decrypt = Object.assign(decryptBytes, {
1606
1678
  });
1607
1679
  async function decryptBytes(keypair, message, opts) {
1608
1680
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1609
- throw new Error("malformed message: too short");
1681
+ throw new HpkeError(
1682
+ "ERR_MALFORMED_MESSAGE",
1683
+ "malformed message: too short"
1684
+ );
1610
1685
  }
1611
1686
  const wrappedLen = message[0] << 8 | message[1];
1612
1687
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1613
1688
  const ctStart = ivStart + NN;
1614
1689
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1615
- throw new Error("malformed message: too short");
1690
+ throw new HpkeError(
1691
+ "ERR_MALFORMED_MESSAGE",
1692
+ "malformed message: too short"
1693
+ );
1616
1694
  }
1617
1695
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1618
1696
  const iv = message.slice(ivStart, ctStart);
@@ -1623,11 +1701,20 @@ async function decryptBytes(keypair, message, opts) {
1623
1701
  opts?.info !== void 0 ? { info: opts.info } : void 0
1624
1702
  );
1625
1703
  const key = await importAesKeyDecryptOnly(keyBytes);
1626
- const pt = await subtle2.decrypt(
1627
- { name: "AES-GCM", iv },
1628
- key,
1629
- ciphertext
1630
- );
1704
+ let pt;
1705
+ try {
1706
+ pt = await subtle2.decrypt(
1707
+ { name: "AES-GCM", iv },
1708
+ key,
1709
+ ciphertext
1710
+ );
1711
+ } catch (err) {
1712
+ throw new HpkeError(
1713
+ "ERR_DECRYPT_FAILED",
1714
+ "failed to decrypt the message: tampered ciphertext",
1715
+ { cause: err }
1716
+ );
1717
+ }
1631
1718
  return new Uint8Array(pt);
1632
1719
  }
1633
1720
  __name(decryptBytes, "decryptBytes");
@@ -1648,7 +1735,8 @@ async function decryptFromString(keypair, message, opts) {
1648
1735
  __name(decryptFromString, "decryptFromString");
1649
1736
  function validateKeysize(keysize) {
1650
1737
  if (keysize !== 128 && keysize !== 256) {
1651
- throw new Error(
1738
+ throw new HpkeError(
1739
+ "ERR_INVALID_KEYSIZE",
1652
1740
  `invalid keysize: ${keysize} (expected 128 or 256)`
1653
1741
  );
1654
1742
  }
@@ -1656,7 +1744,8 @@ function validateKeysize(keysize) {
1656
1744
  __name(validateKeysize, "validateKeysize");
1657
1745
  function validateRawKeyBytes(raw) {
1658
1746
  if (raw.length !== 16 && raw.length !== 32) {
1659
- throw new Error(
1747
+ throw new HpkeError(
1748
+ "ERR_INVALID_AES_KEY",
1660
1749
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1661
1750
  );
1662
1751
  }
@@ -1670,19 +1759,20 @@ function normalizeInfo(info) {
1670
1759
  __name(normalizeInfo, "normalizeInfo");
1671
1760
  async function exportAesKeyBytes(key) {
1672
1761
  if (!key.extractable) {
1673
- throw new Error(
1762
+ throw new HpkeError(
1763
+ "ERR_INVALID_AES_KEY",
1674
1764
  "aesKey must be extractable: its raw bytes are what get sealed"
1675
1765
  );
1676
1766
  }
1677
1767
  return new Uint8Array(await subtle2.exportKey("raw", key));
1678
1768
  }
1679
1769
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1680
- async function importAesKey(raw) {
1770
+ async function importAesKey(raw, extractable) {
1681
1771
  return subtle2.importKey(
1682
1772
  "raw",
1683
1773
  raw,
1684
1774
  { name: "AES-GCM" },
1685
- true,
1775
+ extractable,
1686
1776
  ["encrypt", "decrypt"]
1687
1777
  );
1688
1778
  }
@@ -1737,7 +1827,10 @@ async function keySchedule(sharedSecret, info) {
1737
1827
  __name(keySchedule, "keySchedule");
1738
1828
  async function openRawBytes(keypair, wrapped, opts) {
1739
1829
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1740
- throw new Error("malformed envelope: too short");
1830
+ throw new HpkeError(
1831
+ "ERR_MALFORMED_ENVELOPE",
1832
+ "malformed envelope: too short"
1833
+ );
1741
1834
  }
1742
1835
  const info = normalizeInfo(opts?.info);
1743
1836
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1749,7 +1842,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1749
1842
  __name(openRawBytes, "openRawBytes");
1750
1843
  async function openBytes(keypair, wrapped, opts) {
1751
1844
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1752
- return importAesKey(keyBytes);
1845
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1753
1846
  }
1754
1847
  __name(openBytes, "openBytes");
1755
1848
  async function encryptKey(recipient, aesKey, opts) {
@@ -1773,7 +1866,10 @@ async function encryptKey(recipient, aesKey, opts) {
1773
1866
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1774
1867
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1775
1868
  const wrapped = concat2(enc, ciphertext);
1776
- const aesGcmKey = await importAesKey(keyBytes);
1869
+ const aesGcmKey = await importAesKey(
1870
+ keyBytes,
1871
+ opts?.extractable ?? false
1872
+ );
1777
1873
  return { wrapped, key: aesGcmKey };
1778
1874
  }
1779
1875
  __name(encryptKey, "encryptKey");