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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Hybrid Public Key Encryption
14
14
 
15
15
  1 dependency -- `uint8arrays`.
16
16
 
17
- On every `create`/`seal` call, we generate a fresh ephemeral X25519 keypair,
17
+ On every `create`/`encrypt` call, we generate a fresh ephemeral X25519 keypair,
18
18
  do Diffie-Hellman against the recipient public key, and the key schedule derives
19
19
  an AES-256-GCM key from that shared secret.
20
20
 
@@ -38,6 +38,9 @@ the plaintext is.
38
38
  - [`decrypt`](#decrypt)
39
39
  - [`decrypt.asString`](#decryptasstring)
40
40
  - [`decrypt.fromString`](#decryptfromstring)
41
+ - [Extractable keys](#extractable-keys)
42
+ - [`keyId`](#keyid)
43
+ - [Errors](#errors)
41
44
  - [Modules](#modules)
42
45
  * [ESM](#esm)
43
46
  * [Common JS](#common-js)
@@ -64,7 +67,7 @@ Create an AES key, or encrypt a message.
64
67
  Encrypt an AES key, then recover it later.
65
68
 
66
69
  ```ts
67
- import { create, seal, open } from 'simple-hpke'
70
+ import { create, encryptKey, open } from 'simple-hpke'
68
71
 
69
72
  // An X25519 keypair. (asymmetric keypair).
70
73
  // The private key can be non-extractable.
@@ -76,24 +79,30 @@ const keypair = await crypto.subtle.generateKey(
76
79
  )
77
80
 
78
81
  // Create a new AES key and encrypt it to your public key.
79
- const { enc, key } = await create(keypair)
82
+ // `key` is the unencrypted new AES key
83
+ // `wrapped` is the AES key encrypted to the given public key
84
+ const { wrapped, key } = await create(keypair.publicKey) // or bytes or string
85
+
86
+ // `key` is non-extractable by default. It still encrypts and decrypts;
87
+ // it just cannot be exported. Pass `{ extractable: true }` if you need
88
+ // the raw bytes. See [Extractable keys](#extractable-keys).
80
89
 
81
90
  //
82
91
  // Or wrap an existing AES key. The supplied AES key must be extractable.
83
92
  //
84
93
  const aesKey = await crypto.subtle.generateKey(
85
94
  { name: 'AES-GCM', length: 256 },
86
- true, // not extractable
95
+ true, // extractable -- its raw bytes are what get sealed
87
96
  ['encrypt', 'decrypt']
88
97
  )
89
98
 
90
99
  // Wrap the existing key.
91
- const { enc: sealedKey } = await seal(keypair, aesKey)
100
+ const { wrapped: wrappedKey } = await encryptKey(keypair, aesKey)
92
101
 
93
102
  // or pass in just a public key
94
103
 
95
104
  // Later, recover the same key with your private key.
96
- const recoveredKey = await open(keypair, sealedKey)
105
+ const recoveredKey = await open(keypair, wrappedKey)
97
106
 
98
107
  // `recoveredKey` is equal to `aesKey`
99
108
  ```
@@ -105,7 +114,7 @@ const recoveredKey = await open(keypair, sealedKey)
105
114
  ### Hybrid Encryption
106
115
 
107
116
  Encrypt a message with AES-GCM, then encrypt the AES key to a given
108
- public key. The wrapped key is concattenated with the cipher text,
117
+ public key. The encrypted key is concattenated with the cipher text,
109
118
  along with the IV. The recipient uses their private key to open the AES key and
110
119
  decrypt the message.
111
120
 
@@ -119,7 +128,7 @@ const recipient = await crypto.subtle.generateKey(
119
128
  )
120
129
 
121
130
  // Create a fresh AES-GCM key, and encrypt a message with it.
122
- const { enc, key } = await create(recipient)
131
+ const { wrapped, key } = await create(recipient)
123
132
  const iv = crypto.getRandomValues(new Uint8Array(12))
124
133
  const ciphertextBuffer = await crypto.subtle.encrypt(
125
134
  { name: 'AES-GCM', iv },
@@ -127,25 +136,28 @@ const ciphertextBuffer = await crypto.subtle.encrypt(
127
136
  new TextEncoder().encode('attack at dawn')
128
137
  )
129
138
 
130
- // Send `enc`, `iv`, and `ciphertext` together. `create(...)` defaults to a
131
- // 256-bit AES key, so `enc.length` is 80 bytes in this example.
139
+ // Send `wrapped`, `iv`, and `ciphertext` together. `create(...)` defaults
140
+ // to a 256-bit AES key, so `wrapped.length` is 80 bytes in this example.
132
141
  //
133
- // If your protocol allows different wrapped-key sizes, prefix `enc.length`
134
- // (or otherwise transmit it) so the recipient can split the payload safely.
142
+ // If your protocol allows different wrapped-key sizes, prefix
143
+ // `wrapped.length` (or otherwise transmit it) so the recipient can split
144
+ // the payload safely.
135
145
  const ciphertext = new Uint8Array(ciphertextBuffer)
136
- const encLength = enc.length
137
- const message = new Uint8Array(encLength + iv.length + ciphertext.length)
138
- message.set(enc, 0)
139
- message.set(iv, encLength)
140
- message.set(ciphertext, encLength + iv.length)
146
+ const wrappedLength = wrapped.length
147
+ const message = new Uint8Array(
148
+ wrappedLength + iv.length + ciphertext.length
149
+ )
150
+ message.set(wrapped, 0)
151
+ message.set(iv, wrappedLength)
152
+ message.set(ciphertext, wrappedLength + iv.length)
141
153
 
142
154
  // On the other side, split the payload back into its parts.
143
- const enc2 = message.subarray(0, encLength)
144
- const iv2 = message.subarray(encLength, encLength + 12)
145
- const ciphertext2 = message.subarray(encLength + 12)
155
+ const wrapped2 = message.subarray(0, wrappedLength)
156
+ const iv2 = message.subarray(wrappedLength, wrappedLength + 12)
157
+ const ciphertext2 = message.subarray(wrappedLength + 12)
146
158
 
147
159
  // Recover the key, then decrypt the message.
148
- const recovered = await open(recipient, enc2)
160
+ const recovered = await open(recipient, wrapped2)
149
161
  const plaintext = await crypto.subtle.decrypt(
150
162
  { name: 'AES-GCM', iv: iv2 },
151
163
  recovered,
@@ -160,7 +172,7 @@ new TextDecoder().decode(plaintext) // => 'attack at dawn'
160
172
  So that was a lot of code to encrypt and decrypt a message...
161
173
  This package exposes functions `encrypt` and `decrypt` that do the same thing.
162
174
 
163
- `encrypt` seals an AES key to the recipient, encrypts the message under
175
+ `encrypt` wraps an AES key to the recipient, encrypts the message under
164
176
  that key, and returns a single envelope:
165
177
  `wrappedLen + wrappedKey + iv + ciphertext`
166
178
  (a 2-byte length prefix, the wrapped key, the 12-byte AES-GCM IV, and the
@@ -332,6 +344,100 @@ decrypt.fromString = async function decryptFromString (
332
344
 
333
345
  ---------------------------------------------------------------
334
346
 
347
+ ## Extractable keys
348
+
349
+ `create`, `encryptKey`, and `open` return a **non-extractable** AES-GCM
350
+ `CryptoKey` by default. It encrypts and decrypts normally; it just cannot
351
+ be exported, so its raw bytes cannot leak out of the runtime.
352
+
353
+ Opt in when you genuinely need the bytes:
354
+
355
+ ```ts
356
+ const { wrapped, key } = await create(recipient, { extractable: true })
357
+ const bytes = new Uint8Array(await crypto.subtle.exportKey('raw', key))
358
+
359
+ // same option on the way back out
360
+ const recovered = await open(keypair, wrapped, { extractable: true })
361
+ ```
362
+
363
+ This is separate from the `aesKey` you may pass **in** to `encryptKey` /
364
+ `encrypt`. That one must always be extractable, because its raw bytes are
365
+ what get sealed.
366
+
367
+ If you only need to tell two keys apart, you do not need the bytes --
368
+ use [`keyId`](#keyid).
369
+
370
+ ## `keyId`
371
+
372
+ A stable fingerprint for an AES-GCM key, so you can ask "is this the key I
373
+ have?" without holding its bytes.
374
+
375
+ ```ts
376
+ async function keyId (key:CryptoKey):Promise<string>
377
+ ```
378
+
379
+ ```ts
380
+ import { create, open, keyId } from 'simple-hpke'
381
+
382
+ const { wrapped, key } = await create(recipient)
383
+ const id = await keyId(key) // 43-char base64url string
384
+
385
+ const recovered = await open(keypair, wrapped)
386
+ await keyId(recovered) === id // true
387
+ ```
388
+
389
+ It works on a **non-extractable** key -- including one rehydrated from
390
+ IndexedDB -- which is the whole point: the obvious implementation
391
+ (`exportKey('raw')` then SHA-256) would force every key you want to
392
+ identify to be extractable.
393
+
394
+ The derivation is fixed, and is treated as wire format:
395
+
396
+ ```
397
+ LABEL = utf8("simple-hpke/keyId/v1")
398
+ N = SHA-256(LABEL)[0..12] // fixed nonce
399
+ C = AES-GCM(key, N, LABEL) // ciphertext || 16-byte tag
400
+ keyId = base64url(SHA-256(LABEL || C))
401
+ ```
402
+
403
+ `key` must be an AES-GCM key with the `encrypt` usage; anything else
404
+ throws [`ERR_INVALID_AES_KEY`](#errors).
405
+
406
+ ## Errors
407
+
408
+ Every error this package throws is an `HpkeError`: an ordinary `Error`
409
+ with a stable `code`. Branch on the code, not on the message text -- the
410
+ codes are part of the API, the prose is not. Where a WebCrypto failure is
411
+ being wrapped, the original is kept on `cause`.
412
+
413
+ ```ts
414
+ import { decrypt, HpkeError } from 'simple-hpke'
415
+
416
+ try {
417
+ await decrypt(keypair, envelope)
418
+ } catch (err) {
419
+ if (err instanceof HpkeError && err.code === 'ERR_DECRYPT_FAILED') {
420
+ // wrong keypair, wrong `info`, or a tampered envelope
421
+ }
422
+ throw err
423
+ }
424
+ ```
425
+
426
+ | Code | Raised when |
427
+ | --- | --- |
428
+ | `ERR_INVALID_RECIPIENT_KEY` | The recipient is not a usable X25519 public key: wrong length, a private key, the wrong algorithm, or an unrecognized form. |
429
+ | `ERR_INVALID_KEYPAIR` | `keypair.privateKey` is not an X25519 private key. |
430
+ | `ERR_SMALL_ORDER_KEY` | The X25519 shared secret came out all-zero (a small-order public key). Conforming runtimes reject this first. |
431
+ | `ERR_MALFORMED_ENVELOPE` | An `open` / `open.raw` envelope is too short to hold `enc` plus an AEAD tag. |
432
+ | `ERR_MALFORMED_MESSAGE` | A `decrypt` envelope is too short to hold its own declared segments. |
433
+ | `ERR_INVALID_KEYSIZE` | `opts.size` is neither `128` nor `256`. |
434
+ | `ERR_INVALID_AES_KEY` | A supplied `aesKey` is the wrong length or non-extractable, or `keyId` got a key it cannot use. |
435
+ | `ERR_DECRYPT_FAILED` | AES-GCM authentication failed: wrong keypair, wrong `info`, or a tampered ciphertext. |
436
+
437
+ `HpkeErrorCode` is exported as a type if you want to switch exhaustively.
438
+
439
+ ---------------------------------------------------------------
440
+
335
441
  ## Modules
336
442
 
337
443
  This exposes ESM and common JS via
@@ -339,7 +445,15 @@ This exposes ESM and common JS via
339
445
 
340
446
  ### ESM
341
447
  ```js
342
- import { create, seal, open, encrypt, decrypt } from 'simple-hpke'
448
+ import {
449
+ create,
450
+ encryptKey,
451
+ open,
452
+ encrypt,
453
+ decrypt,
454
+ keyId,
455
+ HpkeError
456
+ } from 'simple-hpke'
343
457
  ```
344
458
 
345
459
  ### Common JS
@@ -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,18 +1317,21 @@ 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
- async function create2(recipient, opts) {
1321
- const { wrapped, key } = await encryptKey(recipient, null, opts);
1322
- return { enc: wrapped, key };
1323
- }
1324
- __name(create2, "create");
1325
- async function seal(recipient, aesKey, opts) {
1326
- const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1327
- return { enc: wrapped, key };
1328
- }
1329
- __name(seal, "seal");
1330
1335
  var open = Object.assign(openBytes, {
1331
1336
  raw: openRawBytes
1332
1337
  });
@@ -1339,13 +1344,19 @@ var decrypt = Object.assign(decryptBytes, {
1339
1344
  });
1340
1345
  async function decryptBytes(keypair, message, opts) {
1341
1346
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1342
- throw new Error("malformed message: too short");
1347
+ throw new HpkeError(
1348
+ "ERR_MALFORMED_MESSAGE",
1349
+ "malformed message: too short"
1350
+ );
1343
1351
  }
1344
1352
  const wrappedLen = message[0] << 8 | message[1];
1345
1353
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1346
1354
  const ctStart = ivStart + NN;
1347
1355
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1348
- throw new Error("malformed message: too short");
1356
+ throw new HpkeError(
1357
+ "ERR_MALFORMED_MESSAGE",
1358
+ "malformed message: too short"
1359
+ );
1349
1360
  }
1350
1361
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1351
1362
  const iv = message.slice(ivStart, ctStart);
@@ -1356,11 +1367,20 @@ async function decryptBytes(keypair, message, opts) {
1356
1367
  opts?.info !== void 0 ? { info: opts.info } : void 0
1357
1368
  );
1358
1369
  const key = await importAesKeyDecryptOnly(keyBytes);
1359
- const pt = await subtle.decrypt(
1360
- { name: "AES-GCM", iv },
1361
- key,
1362
- ciphertext
1363
- );
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
+ }
1364
1384
  return new Uint8Array(pt);
1365
1385
  }
1366
1386
  __name(decryptBytes, "decryptBytes");
@@ -1381,7 +1401,8 @@ async function decryptFromString(keypair, message, opts) {
1381
1401
  __name(decryptFromString, "decryptFromString");
1382
1402
  function validateKeysize(keysize) {
1383
1403
  if (keysize !== 128 && keysize !== 256) {
1384
- throw new Error(
1404
+ throw new HpkeError(
1405
+ "ERR_INVALID_KEYSIZE",
1385
1406
  `invalid keysize: ${keysize} (expected 128 or 256)`
1386
1407
  );
1387
1408
  }
@@ -1389,7 +1410,8 @@ function validateKeysize(keysize) {
1389
1410
  __name(validateKeysize, "validateKeysize");
1390
1411
  function validateRawKeyBytes(raw) {
1391
1412
  if (raw.length !== 16 && raw.length !== 32) {
1392
- throw new Error(
1413
+ throw new HpkeError(
1414
+ "ERR_INVALID_AES_KEY",
1393
1415
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1394
1416
  );
1395
1417
  }
@@ -1403,19 +1425,20 @@ function normalizeInfo(info) {
1403
1425
  __name(normalizeInfo, "normalizeInfo");
1404
1426
  async function exportAesKeyBytes(key) {
1405
1427
  if (!key.extractable) {
1406
- throw new Error(
1428
+ throw new HpkeError(
1429
+ "ERR_INVALID_AES_KEY",
1407
1430
  "aesKey must be extractable: its raw bytes are what get sealed"
1408
1431
  );
1409
1432
  }
1410
1433
  return new Uint8Array(await subtle.exportKey("raw", key));
1411
1434
  }
1412
1435
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1413
- async function importAesKey(raw) {
1436
+ async function importAesKey(raw, extractable) {
1414
1437
  return subtle.importKey(
1415
1438
  "raw",
1416
1439
  raw,
1417
1440
  { name: "AES-GCM" },
1418
- true,
1441
+ extractable,
1419
1442
  ["encrypt", "decrypt"]
1420
1443
  );
1421
1444
  }
@@ -1470,7 +1493,10 @@ async function keySchedule(sharedSecret, info) {
1470
1493
  __name(keySchedule, "keySchedule");
1471
1494
  async function openRawBytes(keypair, wrapped, opts) {
1472
1495
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1473
- throw new Error("malformed envelope: too short");
1496
+ throw new HpkeError(
1497
+ "ERR_MALFORMED_ENVELOPE",
1498
+ "malformed envelope: too short"
1499
+ );
1474
1500
  }
1475
1501
  const info = normalizeInfo(opts?.info);
1476
1502
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1482,7 +1508,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1482
1508
  __name(openRawBytes, "openRawBytes");
1483
1509
  async function openBytes(keypair, wrapped, opts) {
1484
1510
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1485
- return importAesKey(keyBytes);
1511
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1486
1512
  }
1487
1513
  __name(openBytes, "openBytes");
1488
1514
  async function encryptKey(recipient, aesKey, opts) {
@@ -1506,7 +1532,10 @@ async function encryptKey(recipient, aesKey, opts) {
1506
1532
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1507
1533
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1508
1534
  const wrapped = concat2(enc, ciphertext);
1509
- const aesGcmKey = await importAesKey(keyBytes);
1535
+ const aesGcmKey = await importAesKey(
1536
+ keyBytes,
1537
+ opts?.extractable ?? false
1538
+ );
1510
1539
  return { wrapped, key: aesGcmKey };
1511
1540
  }
1512
1541
  __name(encryptKey, "encryptKey");
@@ -1537,34 +1566,38 @@ async function aeadOpen(key, nonce, ciphertext) {
1537
1566
  false,
1538
1567
  ["decrypt"]
1539
1568
  );
1540
- const pt = await subtle2.decrypt(
1541
- { name: "AES-GCM", iv: nonce },
1542
- k,
1543
- ciphertext
1544
- );
1569
+ let pt;
1570
+ try {
1571
+ pt = await subtle2.decrypt(
1572
+ { name: "AES-GCM", iv: nonce },
1573
+ k,
1574
+ ciphertext
1575
+ );
1576
+ } catch (err) {
1577
+ throw new HpkeError(
1578
+ "ERR_DECRYPT_FAILED",
1579
+ "failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
1580
+ { cause: err }
1581
+ );
1582
+ }
1545
1583
  return new Uint8Array(pt);
1546
1584
  }
1547
1585
  __name(aeadOpen, "aeadOpen");
1548
1586
  async function encryptBytes(recipient, message, aesKey, opts) {
1549
1587
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1550
- let enc;
1551
- let key;
1552
- if (!aesKey) {
1553
- const keys = await create2(recipient, opts);
1554
- enc = keys.enc;
1555
- key = keys.key;
1556
- } else {
1557
- const keys = await seal(recipient, aesKey, opts);
1558
- enc = keys.enc;
1559
- key = keys.key;
1560
- }
1588
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1561
1589
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1562
1590
  const ct = new Uint8Array(await subtle2.encrypt(
1563
1591
  { name: "AES-GCM", iv },
1564
1592
  key,
1565
1593
  plaintext
1566
1594
  ));
1567
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1595
+ return concat2(
1596
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1597
+ wrapped,
1598
+ iv,
1599
+ ct
1600
+ );
1568
1601
  }
1569
1602
  __name(encryptBytes, "encryptBytes");
1570
1603
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1630,7 +1663,8 @@ async function exportRawPublic(key) {
1630
1663
  __name(exportRawPublic, "exportRawPublic");
1631
1664
  async function importRawPublic(raw) {
1632
1665
  if (raw.length !== ENC_LENGTH) {
1633
- throw new Error(
1666
+ throw new HpkeError(
1667
+ "ERR_INVALID_RECIPIENT_KEY",
1634
1668
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1635
1669
  );
1636
1670
  }
@@ -1649,9 +1683,16 @@ async function resolveRecipientPublicKey(recipient) {
1649
1683
  }
1650
1684
  if (recipient instanceof CryptoKey) {
1651
1685
  if (recipient.type !== "public") {
1652
- throw new Error("recipient CryptoKey must be a public key");
1686
+ throw new HpkeError(
1687
+ "ERR_INVALID_RECIPIENT_KEY",
1688
+ "recipient CryptoKey must be a public key"
1689
+ );
1653
1690
  }
1654
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1691
+ assertX25519Algorithm(
1692
+ recipient,
1693
+ "recipient CryptoKey",
1694
+ "ERR_INVALID_RECIPIENT_KEY"
1695
+ );
1655
1696
  return recipient;
1656
1697
  }
1657
1698
  if (typeof recipient.publicKey === "string") {
@@ -1661,17 +1702,28 @@ async function resolveRecipientPublicKey(recipient) {
1661
1702
  const pair = recipient;
1662
1703
  if (pair.publicKey instanceof CryptoKey) {
1663
1704
  if (pair.publicKey.type !== "public") {
1664
- throw new Error("recipient keypair publicKey must be a public key");
1705
+ throw new HpkeError(
1706
+ "ERR_INVALID_RECIPIENT_KEY",
1707
+ "recipient keypair publicKey must be a public key"
1708
+ );
1665
1709
  }
1666
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1710
+ assertX25519Algorithm(
1711
+ pair.publicKey,
1712
+ "recipient keypair publicKey",
1713
+ "ERR_INVALID_RECIPIENT_KEY"
1714
+ );
1667
1715
  return pair.publicKey;
1668
1716
  }
1669
- throw new Error("unrecognized recipient key form");
1717
+ throw new HpkeError(
1718
+ "ERR_INVALID_RECIPIENT_KEY",
1719
+ "unrecognized recipient key form"
1720
+ );
1670
1721
  }
1671
1722
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1672
- function assertX25519Algorithm(key, context) {
1723
+ function assertX25519Algorithm(key, context, code2) {
1673
1724
  if (key.algorithm.name !== "X25519") {
1674
- throw new Error(
1725
+ throw new HpkeError(
1726
+ code2,
1675
1727
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1676
1728
  );
1677
1729
  }
@@ -1685,7 +1737,8 @@ async function dh(priv, pub) {
1685
1737
  );
1686
1738
  const bytes = new Uint8Array(bits);
1687
1739
  if (bytes.every((b) => b === 0)) {
1688
- throw new Error(
1740
+ throw new HpkeError(
1741
+ "ERR_SMALL_ORDER_KEY",
1689
1742
  "X25519 shared secret is all-zero (small-order public key?)"
1690
1743
  );
1691
1744
  }
@@ -1724,9 +1777,16 @@ async function encap(pkR) {
1724
1777
  __name(encap, "encap");
1725
1778
  async function decap(enc, keypair) {
1726
1779
  if (keypair.privateKey.type !== "private") {
1727
- throw new Error("keypair.privateKey must be a private key");
1780
+ throw new HpkeError(
1781
+ "ERR_INVALID_KEYPAIR",
1782
+ "keypair.privateKey must be a private key"
1783
+ );
1728
1784
  }
1729
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1785
+ assertX25519Algorithm(
1786
+ keypair.privateKey,
1787
+ "keypair.privateKey",
1788
+ "ERR_INVALID_KEYPAIR"
1789
+ );
1730
1790
  const pkE = await subtle2.importKey(
1731
1791
  "raw",
1732
1792
  enc,
@@ -1785,4 +1845,6 @@ var HPKE_SUITE_ID = concat2(
1785
1845
  i2osp(KDF_ID, 2),
1786
1846
  i2osp(AEAD_ID, 2)
1787
1847
  );
1848
+ var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
1849
+ var KEY_ID_NONCE_LENGTH = 12;
1788
1850
  //# sourceMappingURL=constants.cjs.map