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/dist/index.d.ts CHANGED
@@ -1,40 +1,31 @@
1
- import { type RecipientKey, encryptToString, encryptBytes } from './util';
1
+ import { type RecipientKey, encryptToString, encryptBytes, keyId } from './util';
2
+ export { keyId };
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
  /**
18
- * Encrypt the given AES key to the given public key.
19
- *
20
- * @returns {{ enc, key }} The wrapped envelope bytes and a usable AES-GCM
21
- * key with the same raw bytes as `aesKey`.
22
- */
23
- export declare function seal(recipient: RecipientKey, aesKey: CryptoKey | Uint8Array | null, opts?: {
24
- info?: Uint8Array | string;
25
- }): Promise<{
26
- enc: Uint8Array<ArrayBufferLike>;
27
- key: CryptoKey;
28
- }>;
29
- /**
30
- * Recover the AES key wrapped by `create` or `seal`. Call `open(...)` for a
31
- * usable AES-GCM `CryptoKey`, or `open.raw(...)` for the raw key bytes.
21
+ * Recover the AES key wrapped by `create` or `encryptKey`. Call `open(...)`
22
+ * for a usable AES-GCM `CryptoKey`, or `open.raw(...)` for the raw key bytes.
32
23
  */
33
24
  export declare const open: typeof openBytes & {
34
25
  raw: typeof openRawBytes;
35
26
  };
36
27
  /**
37
- * Seal an AES key to `recipient` and AES-GCM encrypt a message under it.
28
+ * Wrap an AES key to `recipient` and AES-GCM encrypt a message under it.
38
29
  * Call `encrypt(...)` for the raw envelope bytes, or `encrypt.asString(...)`
39
30
  * for the same envelope as an encoded string.
40
31
  */
@@ -58,7 +49,7 @@ export declare const decrypt: typeof decryptBytes & {
58
49
  *
59
50
  * @param keypair The same recipient `CryptoKeyPair` used to `encrypt`.
60
51
  * @param message The envelope returned by `encrypt`.
61
- * @param opts `info` — must match the value passed to `encrypt`.
52
+ * @param opts `info` -- must match the value passed to `encrypt`.
62
53
  * @returns The decrypted plaintext bytes. Use `decrypt.asString` for a string.
63
54
  */
64
55
  declare function decryptBytes(keypair: CryptoKeyPair, message: Uint8Array, opts?: {
@@ -70,7 +61,7 @@ declare function decryptBytes(keypair: CryptoKeyPair, message: Uint8Array, opts?
70
61
  *
71
62
  * @param keypair The same recipient `CryptoKeyPair` used to `encrypt`.
72
63
  * @param message The envelope returned by `encrypt`.
73
- * @param opts `info` — must match the value passed to `encrypt`.
64
+ * @param opts `info` -- must match the value passed to `encrypt`.
74
65
  * @returns The decrypted plaintext as a string.
75
66
  */
76
67
  declare function decryptToString(keypair: CryptoKeyPair, message: Uint8Array, opts?: {
@@ -102,25 +93,55 @@ declare function decryptFromString(keypair: CryptoKeyPair, message: string, opts
102
93
  * Like `open`, but returns the recovered key as raw bytes instead of
103
94
  * importing it as an AES-GCM `CryptoKey`. Exposed as `open.raw`.
104
95
  *
105
- * @param keypair The same X25519 `CryptoKeyPair` used to create or seal.
106
- * @param wrapped The `enc` bytes returned by `create` or `seal`.
107
- * @param opts `info` — must match the value passed to `create` or `seal`.
96
+ * @param keypair The same X25519 `CryptoKeyPair` used 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`.
108
100
  * @returns The recovered key bytes (16 or 32 bytes, matching whatever
109
101
  * was wrapped).
110
102
  */
111
103
  declare function openRawBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
112
- info: Uint8Array | string;
104
+ info?: Uint8Array | string;
113
105
  }): Promise<Uint8Array>;
114
106
  /**
115
- * Recover an AES key that was wrapped with `create` or `seal`.
107
+ * Recover an AES key that was wrapped with `create` or `encryptKey`.
116
108
  *
117
- * @param keypair The same X25519 `CryptoKeyPair` used to create or seal.
118
- * @param wrapped The `enc` bytes returned by `create` or `seal`.
119
- * @param opts `info` — must match the value passed to `create` or `seal`.
120
- * @returns The recovered AES-GCM `CryptoKey` (extractable).
109
+ * @param keypair The same X25519 `CryptoKeyPair` used to create or encryptKey.
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.
121
115
  */
122
116
  declare function openBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
123
- info: Uint8Array | string;
117
+ info?: Uint8Array | string;
118
+ extractable?: boolean;
124
119
  }): Promise<CryptoKey>;
125
- export {};
120
+ /**
121
+ * Wrap an AES key to a recipient's public key. `create(...)` calls this
122
+ * helper with no `aesKey`, so it generates a fresh AES key first.
123
+ *
124
+ * @param recipient The recipient's X25519 public key, as a `CryptoKey`,
125
+ * `CryptoKeyPair` (its `.publicKey` is used), 32 raw bytes (`Uint8Array`),
126
+ * or `{ publicKey:string, encoding? }` (encoding defaults to `base64url`).
127
+ * @param aesKey Optional key to wrap, as either an AES-GCM `CryptoKey` or its
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) -- that is an input constraint, unrelated to
131
+ * `opts.extractable`, which governs the key this returns.
132
+ * @param opts `size` (128/256, default 256; ignored when `aesKey` is
133
+ * supplied), `info` (bound into the HPKE key schedule; default empty),
134
+ * and `extractable` (whether the RETURNED key can be exported; default
135
+ * false).
136
+ * @returns { wrapped:Uint8Array, key:CryptoKey } The wrapped envelope
137
+ * bytes and a usable AES-GCM `CryptoKey`.
138
+ */
139
+ export declare function encryptKey(recipient: RecipientKey, aesKey?: CryptoKey | Uint8Array | null, opts?: {
140
+ size?: 128 | 256;
141
+ info?: Uint8Array | string;
142
+ extractable?: boolean;
143
+ }): Promise<{
144
+ wrapped: Uint8Array;
145
+ key: CryptoKey;
146
+ }>;
126
147
  //# 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;;;;;GAKG;AACH,wBAAsB,IAAI,CACtB,SAAS,EAAC,YAAY,EACtB,MAAM,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EAChC,IAAI,CAAC,EAAC;IAEF,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,YAAY,EAGjB,eAAe,EACf,YAAY,EAOZ,KAAK,EACR,MAAM,QAAQ,CAAA;AAYf,OAAO,EAAE,KAAK,EAAE,CAAA;AAChB,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAWxD;;;;;;;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;IAEvB,WAAW,CAAC,EAAC,OAAO,CAAA;CACvB,GACH,OAAO,CAAC;IAAE,OAAO,EAAC,UAAU,CAAC,eAAe,CAAC,CAAC;IAAC,GAAG,EAAC,SAAS,CAAA;CAAE,CAAC,CAGhE;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,CA+CpB;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;AAmIrB;;;;;;;;;;GAUG;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,CAepB;AAED;;;;;;;;;GASG;AACH,iBAAe,SAAS,CACpB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAC;IAAC,WAAW,CAAC,EAAC,OAAO,CAAA;CAAE,GACzD,OAAO,CAAC,SAAS,CAAC,CAGnB;AAED;;;;;;;;;;;;;;;;;;GAkBG;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;IAEvB,WAAW,CAAC,EAAC,OAAO,CAAA;CACvB,GACH,OAAO,CAAC;IAAE,OAAO,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,SAAS,CAAA;CAAE,CAAC,CA6B/C"}
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,34 +1347,38 @@ 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) {
1344
1368
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1345
- let enc;
1346
- let key;
1347
- if (!aesKey) {
1348
- const keys = await create2(recipient, opts);
1349
- enc = keys.enc;
1350
- key = keys.key;
1351
- } else {
1352
- const keys = await seal(recipient, aesKey, opts);
1353
- enc = keys.enc;
1354
- key = keys.key;
1355
- }
1369
+ const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1356
1370
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
1357
1371
  const ct = new Uint8Array(await subtle.encrypt(
1358
1372
  { name: "AES-GCM", iv },
1359
1373
  key,
1360
1374
  plaintext
1361
1375
  ));
1362
- return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
1376
+ return concat2(
1377
+ i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
1378
+ wrapped,
1379
+ iv,
1380
+ ct
1381
+ );
1363
1382
  }
1364
1383
  __name(encryptBytes, "encryptBytes");
1365
1384
  async function encryptToString(recipient, message, aesKey, opts) {
@@ -1425,7 +1444,8 @@ async function exportRawPublic(key) {
1425
1444
  __name(exportRawPublic, "exportRawPublic");
1426
1445
  async function importRawPublic(raw) {
1427
1446
  if (raw.length !== ENC_LENGTH) {
1428
- throw new Error(
1447
+ throw new HpkeError(
1448
+ "ERR_INVALID_RECIPIENT_KEY",
1429
1449
  `invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
1430
1450
  );
1431
1451
  }
@@ -1444,9 +1464,16 @@ async function resolveRecipientPublicKey(recipient) {
1444
1464
  }
1445
1465
  if (recipient instanceof CryptoKey) {
1446
1466
  if (recipient.type !== "public") {
1447
- throw new Error("recipient CryptoKey must be a public key");
1467
+ throw new HpkeError(
1468
+ "ERR_INVALID_RECIPIENT_KEY",
1469
+ "recipient CryptoKey must be a public key"
1470
+ );
1448
1471
  }
1449
- assertX25519Algorithm(recipient, "recipient CryptoKey");
1472
+ assertX25519Algorithm(
1473
+ recipient,
1474
+ "recipient CryptoKey",
1475
+ "ERR_INVALID_RECIPIENT_KEY"
1476
+ );
1450
1477
  return recipient;
1451
1478
  }
1452
1479
  if (typeof recipient.publicKey === "string") {
@@ -1456,17 +1483,28 @@ async function resolveRecipientPublicKey(recipient) {
1456
1483
  const pair = recipient;
1457
1484
  if (pair.publicKey instanceof CryptoKey) {
1458
1485
  if (pair.publicKey.type !== "public") {
1459
- throw new Error("recipient keypair publicKey must be a public key");
1486
+ throw new HpkeError(
1487
+ "ERR_INVALID_RECIPIENT_KEY",
1488
+ "recipient keypair publicKey must be a public key"
1489
+ );
1460
1490
  }
1461
- assertX25519Algorithm(pair.publicKey, "recipient keypair publicKey");
1491
+ assertX25519Algorithm(
1492
+ pair.publicKey,
1493
+ "recipient keypair publicKey",
1494
+ "ERR_INVALID_RECIPIENT_KEY"
1495
+ );
1462
1496
  return pair.publicKey;
1463
1497
  }
1464
- throw new Error("unrecognized recipient key form");
1498
+ throw new HpkeError(
1499
+ "ERR_INVALID_RECIPIENT_KEY",
1500
+ "unrecognized recipient key form"
1501
+ );
1465
1502
  }
1466
1503
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1467
- function assertX25519Algorithm(key, context) {
1504
+ function assertX25519Algorithm(key, context, code2) {
1468
1505
  if (key.algorithm.name !== "X25519") {
1469
- throw new Error(
1506
+ throw new HpkeError(
1507
+ code2,
1470
1508
  `${context} must be an X25519 key (got ${key.algorithm.name})`
1471
1509
  );
1472
1510
  }
@@ -1480,7 +1518,8 @@ async function dh(priv, pub) {
1480
1518
  );
1481
1519
  const bytes = new Uint8Array(bits);
1482
1520
  if (bytes.every((b) => b === 0)) {
1483
- throw new Error(
1521
+ throw new HpkeError(
1522
+ "ERR_SMALL_ORDER_KEY",
1484
1523
  "X25519 shared secret is all-zero (small-order public key?)"
1485
1524
  );
1486
1525
  }
@@ -1519,9 +1558,16 @@ async function encap(pkR) {
1519
1558
  __name(encap, "encap");
1520
1559
  async function decap(enc, keypair) {
1521
1560
  if (keypair.privateKey.type !== "private") {
1522
- throw new Error("keypair.privateKey must be a private key");
1561
+ throw new HpkeError(
1562
+ "ERR_INVALID_KEYPAIR",
1563
+ "keypair.privateKey must be a private key"
1564
+ );
1523
1565
  }
1524
- assertX25519Algorithm(keypair.privateKey, "keypair.privateKey");
1566
+ assertX25519Algorithm(
1567
+ keypair.privateKey,
1568
+ "keypair.privateKey",
1569
+ "ERR_INVALID_KEYPAIR"
1570
+ );
1525
1571
  const pkE = await subtle.importKey(
1526
1572
  "raw",
1527
1573
  enc,
@@ -1535,6 +1581,35 @@ async function decap(enc, keypair) {
1535
1581
  return extractAndExpand(dhBytes, kemContext);
1536
1582
  }
1537
1583
  __name(decap, "decap");
1584
+ async function keyId(key) {
1585
+ if (key.algorithm.name !== "AES-GCM") {
1586
+ throw new HpkeError(
1587
+ "ERR_INVALID_AES_KEY",
1588
+ `keyId needs an AES-GCM key (got ${key.algorithm.name})`
1589
+ );
1590
+ }
1591
+ if (!key.usages.includes("encrypt")) {
1592
+ throw new HpkeError(
1593
+ "ERR_INVALID_AES_KEY",
1594
+ "keyId needs a key with the 'encrypt' usage"
1595
+ );
1596
+ }
1597
+ const labelHash = new Uint8Array(
1598
+ await subtle.digest("SHA-256", KEY_ID_LABEL)
1599
+ );
1600
+ const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
1601
+ const ciphertext = new Uint8Array(await subtle.encrypt(
1602
+ { name: "AES-GCM", iv: nonce },
1603
+ key,
1604
+ KEY_ID_LABEL
1605
+ ));
1606
+ const digest2 = await subtle.digest(
1607
+ "SHA-256",
1608
+ concat2(KEY_ID_LABEL, ciphertext)
1609
+ );
1610
+ return toString2(new Uint8Array(digest2), "base64url");
1611
+ }
1612
+ __name(keyId, "keyId");
1538
1613
  function i2osp(n, len) {
1539
1614
  const out = new Uint8Array(len);
1540
1615
  let v = n;
@@ -1562,14 +1637,9 @@ __name(concat2, "concat");
1562
1637
  var subtle2 = globalThis.crypto.subtle;
1563
1638
  async function create2(recipient, opts) {
1564
1639
  const { wrapped, key } = await encryptKey(recipient, null, opts);
1565
- return { enc: wrapped, key };
1640
+ return { wrapped, key };
1566
1641
  }
1567
1642
  __name(create2, "create");
1568
- async function seal(recipient, aesKey, opts) {
1569
- const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1570
- return { enc: wrapped, key };
1571
- }
1572
- __name(seal, "seal");
1573
1643
  var open = Object.assign(openBytes, {
1574
1644
  raw: openRawBytes
1575
1645
  });
@@ -1582,13 +1652,19 @@ var decrypt = Object.assign(decryptBytes, {
1582
1652
  });
1583
1653
  async function decryptBytes(keypair, message, opts) {
1584
1654
  if (message.byteLength < WRAPPED_LEN_PREFIX) {
1585
- throw new Error("malformed message: too short");
1655
+ throw new HpkeError(
1656
+ "ERR_MALFORMED_MESSAGE",
1657
+ "malformed message: too short"
1658
+ );
1586
1659
  }
1587
1660
  const wrappedLen = message[0] << 8 | message[1];
1588
1661
  const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
1589
1662
  const ctStart = ivStart + NN;
1590
1663
  if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
1591
- throw new Error("malformed message: too short");
1664
+ throw new HpkeError(
1665
+ "ERR_MALFORMED_MESSAGE",
1666
+ "malformed message: too short"
1667
+ );
1592
1668
  }
1593
1669
  const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
1594
1670
  const iv = message.slice(ivStart, ctStart);
@@ -1599,11 +1675,20 @@ async function decryptBytes(keypair, message, opts) {
1599
1675
  opts?.info !== void 0 ? { info: opts.info } : void 0
1600
1676
  );
1601
1677
  const key = await importAesKeyDecryptOnly(keyBytes);
1602
- const pt = await subtle2.decrypt(
1603
- { name: "AES-GCM", iv },
1604
- key,
1605
- ciphertext
1606
- );
1678
+ let pt;
1679
+ try {
1680
+ pt = await subtle2.decrypt(
1681
+ { name: "AES-GCM", iv },
1682
+ key,
1683
+ ciphertext
1684
+ );
1685
+ } catch (err) {
1686
+ throw new HpkeError(
1687
+ "ERR_DECRYPT_FAILED",
1688
+ "failed to decrypt the message: tampered ciphertext",
1689
+ { cause: err }
1690
+ );
1691
+ }
1607
1692
  return new Uint8Array(pt);
1608
1693
  }
1609
1694
  __name(decryptBytes, "decryptBytes");
@@ -1624,7 +1709,8 @@ async function decryptFromString(keypair, message, opts) {
1624
1709
  __name(decryptFromString, "decryptFromString");
1625
1710
  function validateKeysize(keysize) {
1626
1711
  if (keysize !== 128 && keysize !== 256) {
1627
- throw new Error(
1712
+ throw new HpkeError(
1713
+ "ERR_INVALID_KEYSIZE",
1628
1714
  `invalid keysize: ${keysize} (expected 128 or 256)`
1629
1715
  );
1630
1716
  }
@@ -1632,7 +1718,8 @@ function validateKeysize(keysize) {
1632
1718
  __name(validateKeysize, "validateKeysize");
1633
1719
  function validateRawKeyBytes(raw) {
1634
1720
  if (raw.length !== 16 && raw.length !== 32) {
1635
- throw new Error(
1721
+ throw new HpkeError(
1722
+ "ERR_INVALID_AES_KEY",
1636
1723
  `invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
1637
1724
  );
1638
1725
  }
@@ -1646,19 +1733,20 @@ function normalizeInfo(info) {
1646
1733
  __name(normalizeInfo, "normalizeInfo");
1647
1734
  async function exportAesKeyBytes(key) {
1648
1735
  if (!key.extractable) {
1649
- throw new Error(
1736
+ throw new HpkeError(
1737
+ "ERR_INVALID_AES_KEY",
1650
1738
  "aesKey must be extractable: its raw bytes are what get sealed"
1651
1739
  );
1652
1740
  }
1653
1741
  return new Uint8Array(await subtle2.exportKey("raw", key));
1654
1742
  }
1655
1743
  __name(exportAesKeyBytes, "exportAesKeyBytes");
1656
- async function importAesKey(raw) {
1744
+ async function importAesKey(raw, extractable) {
1657
1745
  return subtle2.importKey(
1658
1746
  "raw",
1659
1747
  raw,
1660
1748
  { name: "AES-GCM" },
1661
- true,
1749
+ extractable,
1662
1750
  ["encrypt", "decrypt"]
1663
1751
  );
1664
1752
  }
@@ -1713,7 +1801,10 @@ async function keySchedule(sharedSecret, info) {
1713
1801
  __name(keySchedule, "keySchedule");
1714
1802
  async function openRawBytes(keypair, wrapped, opts) {
1715
1803
  if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
1716
- throw new Error("malformed envelope: too short");
1804
+ throw new HpkeError(
1805
+ "ERR_MALFORMED_ENVELOPE",
1806
+ "malformed envelope: too short"
1807
+ );
1717
1808
  }
1718
1809
  const info = normalizeInfo(opts?.info);
1719
1810
  const enc = wrapped.slice(0, ENC_LENGTH);
@@ -1725,7 +1816,7 @@ async function openRawBytes(keypair, wrapped, opts) {
1725
1816
  __name(openRawBytes, "openRawBytes");
1726
1817
  async function openBytes(keypair, wrapped, opts) {
1727
1818
  const keyBytes = await openRawBytes(keypair, wrapped, opts);
1728
- return importAesKey(keyBytes);
1819
+ return importAesKey(keyBytes, opts?.extractable ?? false);
1729
1820
  }
1730
1821
  __name(openBytes, "openBytes");
1731
1822
  async function encryptKey(recipient, aesKey, opts) {
@@ -1749,15 +1840,20 @@ async function encryptKey(recipient, aesKey, opts) {
1749
1840
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1750
1841
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1751
1842
  const wrapped = concat2(enc, ciphertext);
1752
- const aesGcmKey = await importAesKey(keyBytes);
1843
+ const aesGcmKey = await importAesKey(
1844
+ keyBytes,
1845
+ opts?.extractable ?? false
1846
+ );
1753
1847
  return { wrapped, key: aesGcmKey };
1754
1848
  }
1755
1849
  __name(encryptKey, "encryptKey");
1756
1850
  export {
1851
+ HpkeError,
1757
1852
  create2 as create,
1758
1853
  decrypt,
1759
1854
  encrypt,
1760
- open,
1761
- seal
1855
+ encryptKey,
1856
+ keyId,
1857
+ open
1762
1858
  };
1763
1859
  //# sourceMappingURL=index.js.map