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.
package/dist/index.d.ts CHANGED
@@ -1,17 +1,20 @@
1
- import { type RecipientKey, encryptToString, encryptBytes } from './util';
1
+ import { type RecipientKey, encryptToString, encryptBytes, keyId, verifyKeyId } from './util';
2
+ export { keyId, verifyKeyId };
3
+ export { HpkeError, type HpkeErrorCode } from './errors';
2
4
  /**
3
5
  * Create a new AES key for the given public key.
4
6
  *
5
7
  * @param recipient Public key for decryptor
6
- * @param opts `size` and `info`
7
- * @returns {{ enc, key }} The wrapped envelope bytes and the generated
8
+ * @param opts `size`, `info`, and `extractable`
9
+ * @returns {{ wrapped, key }} The wrapped envelope bytes and the generated
8
10
  * AES-GCM key.
9
11
  */
10
12
  export declare function create(recipient: RecipientKey, opts?: {
11
13
  size?: 128 | 256;
12
14
  info?: Uint8Array | string;
15
+ extractable?: boolean;
13
16
  }): Promise<{
14
- enc: Uint8Array<ArrayBufferLike>;
17
+ wrapped: Uint8Array<ArrayBufferLike>;
15
18
  key: CryptoKey;
16
19
  }>;
17
20
  /**
@@ -46,7 +49,7 @@ export declare const decrypt: typeof decryptBytes & {
46
49
  *
47
50
  * @param keypair The same recipient `CryptoKeyPair` used to `encrypt`.
48
51
  * @param message The envelope returned by `encrypt`.
49
- * @param opts `info` — must match the value passed to `encrypt`.
52
+ * @param opts `info` -- must match the value passed to `encrypt`.
50
53
  * @returns The decrypted plaintext bytes. Use `decrypt.asString` for a string.
51
54
  */
52
55
  declare function decryptBytes(keypair: CryptoKeyPair, message: Uint8Array, opts?: {
@@ -58,7 +61,7 @@ declare function decryptBytes(keypair: CryptoKeyPair, message: Uint8Array, opts?
58
61
  *
59
62
  * @param keypair The same recipient `CryptoKeyPair` used to `encrypt`.
60
63
  * @param message The envelope returned by `encrypt`.
61
- * @param opts `info` — must match the value passed to `encrypt`.
64
+ * @param opts `info` -- must match the value passed to `encrypt`.
62
65
  * @returns The decrypted plaintext as a string.
63
66
  */
64
67
  declare function decryptToString(keypair: CryptoKeyPair, message: Uint8Array, opts?: {
@@ -91,24 +94,28 @@ declare function decryptFromString(keypair: CryptoKeyPair, message: string, opts
91
94
  * importing it as an AES-GCM `CryptoKey`. Exposed as `open.raw`.
92
95
  *
93
96
  * @param keypair The same X25519 `CryptoKeyPair` used to create or encryptKey.
94
- * @param wrapped The `enc` bytes returned by `create` or `encryptKey`.
95
- * @param opts `info` — must match the value passed to `create` or `encryptKey`.
97
+ * @param wrapped The `wrapped` bytes returned by `create` or `encryptKey`.
98
+ * @param opts `info` -- must match the value passed to `create` or
99
+ * `encryptKey`.
96
100
  * @returns The recovered key bytes (16 or 32 bytes, matching whatever
97
101
  * was wrapped).
98
102
  */
99
103
  declare function openRawBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
100
- info: Uint8Array | string;
104
+ info?: Uint8Array | string;
101
105
  }): Promise<Uint8Array>;
102
106
  /**
103
107
  * Recover an AES key that was wrapped with `create` or `encryptKey`.
104
108
  *
105
109
  * @param keypair The same X25519 `CryptoKeyPair` used to create or encryptKey.
106
- * @param wrapped The `enc` bytes returned by `create` or `encryptKey`.
107
- * @param opts `info` — must match the value passed to `create` or `encryptKey`.
108
- * @returns The recovered AES-GCM `CryptoKey` (extractable).
110
+ * @param wrapped The `wrapped` bytes returned by `create` or `encryptKey`.
111
+ * @param opts `info` -- must match the value passed to `create` or
112
+ * `encryptKey` -- and `extractable` (default false).
113
+ * @returns The recovered AES-GCM `CryptoKey`, non-extractable unless
114
+ * `opts.extractable` is true.
109
115
  */
110
116
  declare function openBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
111
- info: Uint8Array | string;
117
+ info?: Uint8Array | string;
118
+ extractable?: boolean;
112
119
  }): Promise<CryptoKey>;
113
120
  /**
114
121
  * Wrap an AES key to a recipient's public key. `create(...)` calls this
@@ -118,20 +125,26 @@ declare function openBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
118
125
  * `CryptoKeyPair` (its `.publicKey` is used), 32 raw bytes (`Uint8Array`),
119
126
  * or `{ publicKey:string, encoding? }` (encoding defaults to `base64url`).
120
127
  * @param aesKey Optional key to wrap, as either an AES-GCM `CryptoKey` or its
121
- * raw bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh
122
- * extractable key of `opts.size` bits. A supplied `CryptoKey` MUST be
123
- * extractable (its raw bytes are wrapped).
128
+ * raw bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh key
129
+ * of `opts.size` bits. A supplied `CryptoKey` MUST be extractable (its
130
+ * raw bytes are wrapped) and is returned as-is: the same object, with
131
+ * its own `extractable` and usages. It does not need the `encrypt`
132
+ * usage, since wrapping never encrypts with it.
124
133
  * @param opts `size` (128/256, default 256; ignored when `aesKey` is
125
- * supplied) and `info` (bound into the HPKE key schedule; default empty).
126
- * @returns { enc:Uint8Array, key:CryptoKey } The wrapped envelope bytes
127
- * and a usable AES-GCM `CryptoKey`.
134
+ * supplied), `info` (bound into the HPKE key schedule; default empty),
135
+ * and `extractable` (whether a key this function imports -- a
136
+ * generated key or raw `Uint8Array` bytes -- can be exported; default
137
+ * false). `extractable` does not apply to a supplied `CryptoKey`.
138
+ * @returns { wrapped:Uint8Array, key:CryptoKey } The wrapped envelope
139
+ * bytes and the AES-GCM `CryptoKey`: the supplied `CryptoKey` itself
140
+ * when one was given, otherwise a newly imported key.
128
141
  */
129
142
  export declare function encryptKey(recipient: RecipientKey, aesKey?: CryptoKey | Uint8Array | null, opts?: {
130
143
  size?: 128 | 256;
131
144
  info?: Uint8Array | string;
145
+ extractable?: boolean;
132
146
  }): Promise<{
133
- enc: Uint8Array;
147
+ wrapped: Uint8Array;
134
148
  key: CryptoKey;
135
149
  }>;
136
- export {};
137
150
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,YAAY,EAGjB,eAAe,EACf,YAAY,EAOf,MAAM,QAAQ,CAAA;AAoBf;;;;;;;GAOG;AACH,wBAAsB,MAAM,CACxB,SAAS,EAAC,YAAY,EACtB,IAAI,CAAC,EAAC;IAEF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IAEb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAC1B,GACH,OAAO,CAAC;IAAE,GAAG,EAAC,UAAU,CAAC,eAAe,CAAC,CAAC;IAAC,GAAG,EAAC,SAAS,CAAA;CAAE,CAAC,CAG5D;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI;;CAEf,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,OAAO;;CAElB,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO;;;CAGlB,CAAA;AAEF;;;;;;;;GAQG;AACH,iBAAe,YAAY,CACvB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GACnC,OAAO,CAAC,UAAU,CAAC,CA6BpB;AAED;;;;;;;;GAQG;AACH,iBAAe,eAAe,CAC1B,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GACnC,OAAO,CAAC,MAAM,CAAC,CAGhB;AAED;;;;;;;;;;;;;GAaG;AACH,iBAAe,iBAAiB,CAC5B,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,MAAM,EACd,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAC;IAAC,MAAM,CAAC,EAAC,KAAK,CAAA;CAAE,GAClD,OAAO,CAAC,MAAM,CAAC,CAAA;AAEjB,iBAAe,iBAAiB,CAC5B,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,MAAM,EACd,IAAI,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAC;IAAC,MAAM,EAAC,IAAI,CAAA;CAAE,GAC/C,OAAO,CAAC,UAAU,CAAC,CAAA;AA0HrB;;;;;;;;;GASG;AACH,iBAAe,YAAY,CACvB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GAClC,OAAO,CAAC,UAAU,CAAC,CAYpB;AAED;;;;;;;GAOG;AACH,iBAAe,SAAS,CACpB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GAClC,OAAO,CAAC,SAAS,CAAC,CAGnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAC5B,SAAS,EAAC,YAAY,EACtB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IAEF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IAEb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAC1B,GACH,OAAO,CAAC;IAAE,GAAG,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,SAAS,CAAA;CAAE,CAAC,CA0B3C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.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/constants.ts
1287
1300
  var KEM_ID = 32;
1288
1301
  var KDF_ID = 1;
@@ -1305,6 +1318,8 @@ var HPKE_SUITE_ID = concat2(
1305
1318
  i2osp(KDF_ID, 2),
1306
1319
  i2osp(AEAD_ID, 2)
1307
1320
  );
1321
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1322
+ var KEY_ID_NONCE_LENGTH = 12;
1308
1323
 
1309
1324
  // src/util.ts
1310
1325
  var subtle = globalThis.crypto.subtle;
@@ -1332,24 +1347,44 @@ async function aeadOpen(key, nonce, ciphertext) {
1332
1347
  false,
1333
1348
  ["decrypt"]
1334
1349
  );
1335
- const pt = await subtle.decrypt(
1336
- { name: "AES-GCM", iv: nonce },
1337
- k,
1338
- ciphertext
1339
- );
1350
+ let pt;
1351
+ try {
1352
+ pt = await subtle.decrypt(
1353
+ { name: "AES-GCM", iv: nonce },
1354
+ k,
1355
+ ciphertext
1356
+ );
1357
+ } catch (err) {
1358
+ throw new HpkeError(
1359
+ "ERR_DECRYPT_FAILED",
1360
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1361
+ { cause: err }
1362
+ );
1363
+ }
1340
1364
  return new Uint8Array(pt);
1341
1365
  }
1342
1366
  __name(aeadOpen, "aeadOpen");
1343
1367
  async function encryptBytes(recipient, message, aesKey, opts) {
1368
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1369
+ throw new HpkeError(
1370
+ "ERR_INVALID_AES_KEY",
1371
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1372
+ );
1373
+ }
1344
1374
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1345
- const { enc, key } = await encryptKey(recipient, aesKey, opts);
1375
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1346
1376
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1347
1377
  const ct = new Uint8Array(await subtle.encrypt(
1348
1378
  { name: "AES-GCM", iv },
1349
1379
  key,
1350
1380
  plaintext
1351
1381
  ));
1352
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1382
+ return concat2(
1383
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1384
+ wrapped,
1385
+ iv,
1386
+ ct
1387
+ );
1353
1388
  }
1354
1389
  __name(encryptBytes, "encryptBytes");
1355
1390
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1415,7 +1450,8 @@ async function exportRawPublic(key) {
1415
1450
  __name(exportRawPublic, "exportRawPublic");
1416
1451
  async function importRawPublic(raw) {
1417
1452
  if (raw.length !== ENC_LENGTH) {
1418
- throw new Error(
1453
+ throw new HpkeError(
1454
+ "ERR_INVALID_RECIPIENT_KEY",
1419
1455
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1420
1456
  );
1421
1457
  }
@@ -1433,44 +1469,83 @@ async function resolveRecipientPublicKey(recipient) {
1433
1469
  return importRawPublic(recipient);
1434
1470
  }
1435
1471
  if (recipient instanceof CryptoKey) {
1436
- if (recipient.type !== "public") {
1437
- throw new Error("recipient CryptoKey must be a public key");
1438
- }
1439
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1440
- return recipient;
1472
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1441
1473
  }
1442
1474
  if (typeof recipient.publicKey === "string") {
1443
1475
  const { publicKey, encoding } = recipient;
1444
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1476
+ let decoded;
1477
+ try {
1478
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1479
+ } catch (err) {
1480
+ throw new HpkeError(
1481
+ "ERR_INVALID_RECIPIENT_KEY",
1482
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1483
+ { cause: err }
1484
+ );
1485
+ }
1486
+ return importRawPublic(decoded);
1445
1487
  }
1446
1488
  const pair = recipient;
1447
1489
  if (pair.publicKey instanceof CryptoKey) {
1448
- if (pair.publicKey.type !== "public") {
1449
- throw new Error("recipient keypair publicKey must be a public key");
1450
- }
1451
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1452
- return pair.publicKey;
1490
+ return assertRecipientPublicKey(
1491
+ pair.publicKey,
1492
+ "recipient keypair publicKey"
1493
+ );
1453
1494
  }
1454
- throw new Error("unrecognized recipient key form");
1495
+ throw new HpkeError(
1496
+ "ERR_INVALID_RECIPIENT_KEY",
1497
+ "unrecognized recipient key form"
1498
+ );
1455
1499
  }
1456
1500
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1457
- function assertX25519Algorithm(key, context) {
1501
+ function assertRecipientPublicKey(key, context) {
1502
+ if (key.type !== "public") {
1503
+ throw new HpkeError(
1504
+ "ERR_INVALID_RECIPIENT_KEY",
1505
+ `${context} must be a public key`
1506
+ );
1507
+ }
1508
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1509
+ if (!key.extractable) {
1510
+ throw new HpkeError(
1511
+ "ERR_INVALID_RECIPIENT_KEY",
1512
+ `${context} must be extractable (the KEM exports it)`
1513
+ );
1514
+ }
1515
+ return key;
1516
+ }
1517
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1518
+ function assertX25519Algorithm(key, context, code2) {
1458
1519
  if (key.algorithm.name !== "X25519") {
1459
- throw new Error(
1520
+ throw new HpkeError(
1521
+ code2,
1460
1522
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1461
1523
  );
1462
1524
  }
1463
1525
  }
1464
1526
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1465
1527
  async function dh(priv, pub) {
1466
- const bits = await subtle.deriveBits(
1467
- { name: "X25519", public: pub },
1468
- priv,
1469
- 256
1470
- );
1528
+ let bits;
1529
+ try {
1530
+ bits = await subtle.deriveBits(
1531
+ { name: "X25519", public: pub },
1532
+ priv,
1533
+ 256
1534
+ );
1535
+ } catch (err) {
1536
+ if (err?.name === "OperationError") {
1537
+ throw new HpkeError(
1538
+ "ERR_SMALL_ORDER_KEY",
1539
+ "X25519 deriveBits rejected a small-order public key",
1540
+ { cause: err }
1541
+ );
1542
+ }
1543
+ throw err;
1544
+ }
1471
1545
  const bytes = new Uint8Array(bits);
1472
1546
  if (bytes.every((b) => b === 0)) {
1473
- throw new Error(
1547
+ throw new HpkeError(
1548
+ "ERR_SMALL_ORDER_KEY",
1474
1549
  "X25519 shared secret is all-zero (small-order public key?)"
1475
1550
  );
1476
1551
  }
@@ -1509,9 +1584,33 @@ async function encap(pkR) {
1509
1584
  __name(encap, "encap");
1510
1585
  async function decap(enc, keypair) {
1511
1586
  if (keypair.privateKey.type !== "private") {
1512
- 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
+ );
1513
1591
  }
1514
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1592
+ assertX25519Algorithm(
1593
+ keypair.privateKey,
1594
+ "keypair.privateKey",
1595
+ "ERR_INVALID_KEYPAIR"
1596
+ );
1597
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1598
+ throw new HpkeError(
1599
+ "ERR_INVALID_KEYPAIR",
1600
+ "keypair.privateKey must have the deriveBits usage"
1601
+ );
1602
+ }
1603
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1604
+ throw new HpkeError(
1605
+ "ERR_INVALID_KEYPAIR",
1606
+ "keypair.publicKey must be an extractable public key"
1607
+ );
1608
+ }
1609
+ assertX25519Algorithm(
1610
+ keypair.publicKey,
1611
+ "keypair.publicKey",
1612
+ "ERR_INVALID_KEYPAIR"
1613
+ );
1515
1614
  const pkE = await subtle.importKey(
1516
1615
  "raw",
1517
1616
  enc,
@@ -1525,6 +1624,45 @@ async function decap(enc, keypair) {
1525
1624
  return extractAndExpand(dhBytes, kemContext);
1526
1625
  }
1527
1626
  __name(decap, "decap");
1627
+ async function keyId(key) {
1628
+ if (key.algorithm.name !== "AES-GCM") {
1629
+ throw new HpkeError(
1630
+ "ERR_INVALID_AES_KEY",
1631
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1632
+ );
1633
+ }
1634
+ if (!key.usages.includes("encrypt")) {
1635
+ throw new HpkeError(
1636
+ "ERR_INVALID_AES_KEY",
1637
+ "keyId needs a key with the 'encrypt' usage"
1638
+ );
1639
+ }
1640
+ const labelHash = new Uint8Array(
1641
+ await subtle.digest("SHA-256", KEY_ID_LABEL)
1642
+ );
1643
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1644
+ const ciphertext = new Uint8Array(await subtle.encrypt(
1645
+ { name: "AES-GCM", iv: nonce },
1646
+ key,
1647
+ KEY_ID_LABEL
1648
+ ));
1649
+ const digest2 = await subtle.digest(
1650
+ "SHA-256",
1651
+ concat2(KEY_ID_LABEL, ciphertext)
1652
+ );
1653
+ return toString2(new Uint8Array(digest2), "base64url");
1654
+ }
1655
+ __name(keyId, "keyId");
1656
+ async function verifyKeyId(key, expected) {
1657
+ const actual = await keyId(key);
1658
+ if (actual.length !== expected.length) return false;
1659
+ let diff = 0;
1660
+ for (let i = 0; i < actual.length; i++) {
1661
+ diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
1662
+ }
1663
+ return diff === 0;
1664
+ }
1665
+ __name(verifyKeyId, "verifyKeyId");
1528
1666
  function i2osp(n, len) {
1529
1667
  const out = new Uint8Array(len);
1530
1668
  let v = n;
@@ -1551,8 +1689,8 @@ __name(concat2, "concat");
1551
1689
  // src/index.ts
1552
1690
  var subtle2 = globalThis.crypto.subtle;
1553
1691
  async function create2(recipient, opts) {
1554
- const { enc, key } = await encryptKey(recipient, null, opts);
1555
- return { enc, key };
1692
+ const { wrapped, key } = await encryptKey(recipient, null, opts);
1693
+ return { wrapped, key };
1556
1694
  }
1557
1695
  __name(create2, "create");
1558
1696
  var open = Object.assign(openBytes, {
@@ -1567,13 +1705,19 @@ var decrypt = Object.assign(decryptBytes, {
1567
1705
  });
1568
1706
  async function decryptBytes(keypair, message, opts) {
1569
1707
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1570
- throw new Error("malformed message: too short");
1708
+ throw new HpkeError(
1709
+ "ERR_MALFORMED_MESSAGE",
1710
+ "malformed message: too short"
1711
+ );
1571
1712
  }
1572
1713
  const wrappedLen = message[0] << 8 | message[1];
1573
1714
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1574
1715
  const ctStart = ivStart + NN;
1575
1716
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1576
- throw new Error("malformed message: too short");
1717
+ throw new HpkeError(
1718
+ "ERR_MALFORMED_MESSAGE",
1719
+ "malformed message: too short"
1720
+ );
1577
1721
  }
1578
1722
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1579
1723
  const iv = message.slice(ivStart, ctStart);
@@ -1584,11 +1728,20 @@ async function decryptBytes(keypair, message, opts) {
1584
1728
  opts?.info !== void 0 ? { info: opts.info } : void 0
1585
1729
  );
1586
1730
  const key = await importAesKeyDecryptOnly(keyBytes);
1587
- const pt = await subtle2.decrypt(
1588
- { name: "AES-GCM", iv },
1589
- key,
1590
- ciphertext
1591
- );
1731
+ let pt;
1732
+ try {
1733
+ pt = await subtle2.decrypt(
1734
+ { name: "AES-GCM", iv },
1735
+ key,
1736
+ ciphertext
1737
+ );
1738
+ } catch (err) {
1739
+ throw new HpkeError(
1740
+ "ERR_DECRYPT_FAILED",
1741
+ "failed to decrypt the message: tampered ciphertext",
1742
+ { cause: err }
1743
+ );
1744
+ }
1592
1745
  return new Uint8Array(pt);
1593
1746
  }
1594
1747
  __name(decryptBytes, "decryptBytes");
@@ -1598,7 +1751,16 @@ async function decryptToString(keypair, message, opts) {
1598
1751
  }
1599
1752
  __name(decryptToString, "decryptToString");
1600
1753
  async function decryptFromString(keypair, message, opts) {
1601
- const bytes = fromString2(message, "base64url");
1754
+ let bytes;
1755
+ try {
1756
+ bytes = fromString2(message, "base64url");
1757
+ } catch (err) {
1758
+ throw new HpkeError(
1759
+ "ERR_MALFORMED_MESSAGE",
1760
+ "malformed message: not base64url",
1761
+ { cause: err }
1762
+ );
1763
+ }
1602
1764
  const plaintext = await decryptBytes(
1603
1765
  keypair,
1604
1766
  bytes,
@@ -1609,7 +1771,8 @@ async function decryptFromString(keypair, message, opts) {
1609
1771
  __name(decryptFromString, "decryptFromString");
1610
1772
  function validateKeysize(keysize) {
1611
1773
  if (keysize !== 128 && keysize !== 256) {
1612
- throw new Error(
1774
+ throw new HpkeError(
1775
+ "ERR_INVALID_KEYSIZE",
1613
1776
  `invalid keysize: ${keysize} (expected 128 or 256)`
1614
1777
  );
1615
1778
  }
@@ -1617,7 +1780,8 @@ function validateKeysize(keysize) {
1617
1780
  __name(validateKeysize, "validateKeysize");
1618
1781
  function validateRawKeyBytes(raw) {
1619
1782
  if (raw.length !== 16 && raw.length !== 32) {
1620
- throw new Error(
1783
+ throw new HpkeError(
1784
+ "ERR_INVALID_AES_KEY",
1621
1785
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1622
1786
  );
1623
1787
  }
@@ -1631,19 +1795,20 @@ function normalizeInfo(info) {
1631
1795
  __name(normalizeInfo, "normalizeInfo");
1632
1796
  async function exportAesKeyBytes(key) {
1633
1797
  if (!key.extractable) {
1634
- throw new Error(
1798
+ throw new HpkeError(
1799
+ "ERR_INVALID_AES_KEY",
1635
1800
  "aesKey must be extractable: its raw bytes are what get sealed"
1636
1801
  );
1637
1802
  }
1638
1803
  return new Uint8Array(await subtle2.exportKey("raw", key));
1639
1804
  }
1640
1805
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1641
- async function importAesKey(raw) {
1806
+ async function importAesKey(raw, extractable) {
1642
1807
  return subtle2.importKey(
1643
1808
  "raw",
1644
1809
  raw,
1645
1810
  { name: "AES-GCM" },
1646
- true,
1811
+ extractable,
1647
1812
  ["encrypt", "decrypt"]
1648
1813
  );
1649
1814
  }
@@ -1698,7 +1863,10 @@ async function keySchedule(sharedSecret, info) {
1698
1863
  __name(keySchedule, "keySchedule");
1699
1864
  async function openRawBytes(keypair, wrapped, opts) {
1700
1865
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1701
- throw new Error("malformed envelope: too short");
1866
+ throw new HpkeError(
1867
+ "ERR_MALFORMED_ENVELOPE",
1868
+ "malformed envelope: too short"
1869
+ );
1702
1870
  }
1703
1871
  const info = normalizeInfo(opts?.info);
1704
1872
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1710,7 +1878,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1710
1878
  __name(openRawBytes, "openRawBytes");
1711
1879
  async function openBytes(keypair, wrapped, opts) {
1712
1880
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1713
- return importAesKey(keyBytes);
1881
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1714
1882
  }
1715
1883
  __name(openBytes, "openBytes");
1716
1884
  async function encryptKey(recipient, aesKey, opts) {
@@ -1720,6 +1888,12 @@ async function encryptKey(recipient, aesKey, opts) {
1720
1888
  validateRawKeyBytes(aesKey);
1721
1889
  keyBytes = aesKey;
1722
1890
  } else if (aesKey) {
1891
+ if (aesKey.algorithm.name !== "AES-GCM") {
1892
+ throw new HpkeError(
1893
+ "ERR_INVALID_AES_KEY",
1894
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1895
+ );
1896
+ }
1723
1897
  keyBytes = await exportAesKeyBytes(aesKey);
1724
1898
  validateRawKeyBytes(keyBytes);
1725
1899
  } else {
@@ -1734,15 +1908,22 @@ async function encryptKey(recipient, aesKey, opts) {
1734
1908
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1735
1909
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1736
1910
  const wrapped = concat2(enc, ciphertext);
1737
- const aesGcmKey = await importAesKey(keyBytes);
1738
- return { enc: wrapped, key: aesGcmKey };
1911
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1912
+ const aesGcmKey = await importAesKey(
1913
+ keyBytes,
1914
+ opts?.extractable ?? false
1915
+ );
1916
+ return { wrapped, key: aesGcmKey };
1739
1917
  }
1740
1918
  __name(encryptKey, "encryptKey");
1741
1919
  export {
1920
+ HpkeError,
1742
1921
  create2 as create,
1743
1922
  decrypt,
1744
1923
  encrypt,
1745
1924
  encryptKey,
1746
- open
1925
+ keyId,
1926
+ open,
1927
+ verifyKeyId
1747
1928
  };
1748
1929
  //# sourceMappingURL=index.js.map