simple-hpke 0.3.1 → 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/util.d.ts CHANGED
@@ -32,7 +32,11 @@ export declare function aeadOpen(key: Uint8Array, nonce: Uint8Array, ciphertext:
32
32
  * @param message Plaintext to encrypt. A `string` is UTF-8 encoded.
33
33
  * @param aesKey Optional key, as either an AES-GCM `CryptoKey` or its raw
34
34
  * bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh key of
35
- * `opts.size` bits. A supplied `CryptoKey` MUST be extractable. Each
35
+ * `opts.size` bits. A supplied `CryptoKey` MUST be extractable and
36
+ * MUST have the `encrypt` usage (the message is encrypted under it
37
+ * directly; otherwise this throws `ERR_INVALID_AES_KEY`). It is used
38
+ * as-is and never re-imported, so the `opts.extractable` setting that
39
+ * `encryptKey` takes does not apply to it. Each
36
40
  * call picks a fresh random 96-bit IV for the message ciphertext, so
37
41
  * reusing the same `aesKey` across many calls carries the standard
38
42
  * birthday bound for random nonces (collision risk becomes
@@ -60,7 +64,11 @@ export declare function encryptBytes(recipient: RecipientKey, message: Uint8Arra
60
64
  * @param message Plaintext to encrypt. A `string` is UTF-8 encoded.
61
65
  * @param aesKey Optional key, as either an AES-GCM `CryptoKey` or its raw
62
66
  * bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh key of
63
- * `opts.size` bits. A supplied `CryptoKey` MUST be extractable. Each
67
+ * `opts.size` bits. A supplied `CryptoKey` MUST be extractable and
68
+ * MUST have the `encrypt` usage (the message is encrypted under it
69
+ * directly; otherwise this throws `ERR_INVALID_AES_KEY`). It is used
70
+ * as-is and never re-imported, so the `opts.extractable` setting that
71
+ * `encryptKey` takes does not apply to it. Each
64
72
  * call picks a fresh random 96-bit IV for the message ciphertext, so
65
73
  * reusing the same `aesKey` across many calls carries the standard
66
74
  * birthday bound for random nonces (collision risk becomes
@@ -116,6 +124,20 @@ export declare function decap(enc: Uint8Array, keypair: CryptoKeyPair): Promise<
116
124
  * or cannot encrypt.
117
125
  */
118
126
  export declare function keyId(key: CryptoKey): Promise<string>;
127
+ /**
128
+ * Check whether `key` matches a stored `keyId`.
129
+ *
130
+ * Compares in constant time over the string. Errors from `keyId`
131
+ * propagate: a key that cannot be fingerprinted rejects rather than
132
+ * resolving `false`, so "cannot check" never reads as "wrong key".
133
+ *
134
+ * @param key An AES-GCM `CryptoKey` with the `encrypt` usage.
135
+ * @param expected A `keyId` string recorded earlier.
136
+ * @returns `true` if `keyId(key)` equals `expected`, else `false`.
137
+ * @throws {HpkeError} `ERR_INVALID_AES_KEY` if `key` is not an AES-GCM key
138
+ * or cannot encrypt.
139
+ */
140
+ export declare function verifyKeyId(key: CryptoKey, expected: string): Promise<boolean>;
119
141
  export declare function i2osp(n: number, len: number): Uint8Array;
120
142
  export declare function concat(...arrays: Uint8Array[]): Uint8Array;
121
143
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,kBAAkB,EAC1B,MAAM,aAAa,CAAA;AAepB;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAClB,SAAS,GACT,aAAa,GACb,UAAU,GACV;IAAE,SAAS,EAAC,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAAE,CAAA;AAExD;;GAEG;AACH,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,SAAS,EAAC,UAAU,GACtB,OAAO,CAAC,UAAU,CAAC,CAcpB;AAED,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,UAAU,EAAC,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC,CA2BpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,YAAY,CAC9B,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAC1B,GACH,OAAO,CAAC,UAAU,CAAC,CAoBpB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,eAAe,CACjC,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAC/B,GACH,OAAO,CAAC,MAAM,CAAC,CAGhB;AAQD,wBAAsB,IAAI,CAAE,GAAG,EAAC,UAAU,EAAE,IAAI,EAAC,UAAU,GAAE,OAAO,CAAC,UAAU,CAAC,CAS/E;AAiCD,wBAAsB,cAAc,CAChC,OAAO,EAAC,UAAU,EAClB,IAAI,EAAC,UAAU,EACf,KAAK,EAAC,MAAM,EACZ,GAAG,EAAC,UAAU,GAChB,OAAO,CAAC,UAAU,CAAC,CAQpB;AAGD,wBAAsB,aAAa,CAC/B,OAAO,EAAC,UAAU,EAClB,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,MAAM,EACZ,IAAI,EAAC,UAAU,EACf,MAAM,EAAC,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CASpB;AA+BD,wBAAsB,yBAAyB,CAC3C,SAAS,EAAC,YAAY,GACxB,OAAO,CAAC,SAAS,CAAC,CAkDnB;AAmED,wBAAsB,KAAK,CACvB,GAAG,EAAC,SAAS,GACf,OAAO,CAAC;IAAE,YAAY,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,UAAU,CAAA;CAAE,CAAC,CAarD;AAID,wBAAsB,KAAK,CACvB,GAAG,EAAC,UAAU,EACd,OAAO,EAAC,aAAa,GACvB,OAAO,CAAC,UAAU,CAAC,CAwBpB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,KAAK,CAAE,GAAG,EAAC,SAAS,GAAE,OAAO,CAAC,MAAM,CAAC,CAuC1D;AAKD,wBAAgB,KAAK,CAAE,CAAC,EAAC,MAAM,EAAE,GAAG,EAAC,MAAM,GAAE,UAAU,CAQtD;AAED,wBAAgB,MAAM,CAAE,GAAG,MAAM,EAAC,UAAU,EAAE,GAAE,UAAU,CAUzD"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/dist/util.js CHANGED
@@ -1381,7 +1381,16 @@ async function decryptToString(keypair, message, opts) {
1381
1381
  }
1382
1382
  __name(decryptToString, "decryptToString");
1383
1383
  async function decryptFromString(keypair, message, opts) {
1384
- const bytes = fromString2(message, "base64url");
1384
+ let bytes;
1385
+ try {
1386
+ bytes = fromString2(message, "base64url");
1387
+ } catch (err) {
1388
+ throw new HpkeError(
1389
+ "ERR_MALFORMED_MESSAGE",
1390
+ "malformed message: not base64url",
1391
+ { cause: err }
1392
+ );
1393
+ }
1385
1394
  const plaintext = await decryptBytes(
1386
1395
  keypair,
1387
1396
  bytes,
@@ -1509,6 +1518,12 @@ async function encryptKey(recipient, aesKey, opts) {
1509
1518
  validateRawKeyBytes(aesKey);
1510
1519
  keyBytes = aesKey;
1511
1520
  } else if (aesKey) {
1521
+ if (aesKey.algorithm.name !== "AES-GCM") {
1522
+ throw new HpkeError(
1523
+ "ERR_INVALID_AES_KEY",
1524
+ "aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
1525
+ );
1526
+ }
1512
1527
  keyBytes = await exportAesKeyBytes(aesKey);
1513
1528
  validateRawKeyBytes(keyBytes);
1514
1529
  } else {
@@ -1523,6 +1538,7 @@ async function encryptKey(recipient, aesKey, opts) {
1523
1538
  const { key, baseNonce } = await keySchedule(sharedSecret, info);
1524
1539
  const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
1525
1540
  const wrapped = concat2(enc, ciphertext);
1541
+ if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
1526
1542
  const aesGcmKey = await importAesKey(
1527
1543
  keyBytes,
1528
1544
  opts?.extractable ?? false
@@ -1575,6 +1591,12 @@ async function aeadOpen(key, nonce, ciphertext) {
1575
1591
  }
1576
1592
  __name(aeadOpen, "aeadOpen");
1577
1593
  async function encryptBytes(recipient, message, aesKey, opts) {
1594
+ if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
1595
+ throw new HpkeError(
1596
+ "ERR_INVALID_AES_KEY",
1597
+ 'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
1598
+ );
1599
+ }
1578
1600
  const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
1579
1601
  const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
1580
1602
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
@@ -1673,37 +1695,28 @@ async function resolveRecipientPublicKey(recipient) {
1673
1695
  return importRawPublic(recipient);
1674
1696
  }
1675
1697
  if (recipient instanceof CryptoKey) {
1676
- if (recipient.type !== "public") {
1677
- throw new HpkeError(
1678
- "ERR_INVALID_RECIPIENT_KEY",
1679
- "recipient CryptoKey must be a public key"
1680
- );
1681
- }
1682
- assertX25519Algorithm(
1683
- recipient,
1684
- "recipient CryptoKey",
1685
- "ERR_INVALID_RECIPIENT_KEY"
1686
- );
1687
- return recipient;
1698
+ return assertRecipientPublicKey(recipient, "recipient CryptoKey");
1688
1699
  }
1689
1700
  if (typeof recipient.publicKey === "string") {
1690
1701
  const { publicKey, encoding } = recipient;
1691
- return importRawPublic(fromString2(publicKey, encoding ?? "base64url"));
1692
- }
1693
- const pair = recipient;
1694
- if (pair.publicKey instanceof CryptoKey) {
1695
- if (pair.publicKey.type !== "public") {
1702
+ let decoded;
1703
+ try {
1704
+ decoded = fromString2(publicKey, encoding ?? "base64url");
1705
+ } catch (err) {
1696
1706
  throw new HpkeError(
1697
1707
  "ERR_INVALID_RECIPIENT_KEY",
1698
- "recipient keypair publicKey must be a public key"
1708
+ `recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
1709
+ { cause: err }
1699
1710
  );
1700
1711
  }
1701
- assertX25519Algorithm(
1712
+ return importRawPublic(decoded);
1713
+ }
1714
+ const pair = recipient;
1715
+ if (pair.publicKey instanceof CryptoKey) {
1716
+ return assertRecipientPublicKey(
1702
1717
  pair.publicKey,
1703
- "recipient keypair publicKey",
1704
- "ERR_INVALID_RECIPIENT_KEY"
1718
+ "recipient keypair publicKey"
1705
1719
  );
1706
- return pair.publicKey;
1707
1720
  }
1708
1721
  throw new HpkeError(
1709
1722
  "ERR_INVALID_RECIPIENT_KEY",
@@ -1711,6 +1724,23 @@ async function resolveRecipientPublicKey(recipient) {
1711
1724
  );
1712
1725
  }
1713
1726
  __name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
1727
+ function assertRecipientPublicKey(key, context) {
1728
+ if (key.type !== "public") {
1729
+ throw new HpkeError(
1730
+ "ERR_INVALID_RECIPIENT_KEY",
1731
+ `${context} must be a public key`
1732
+ );
1733
+ }
1734
+ assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
1735
+ if (!key.extractable) {
1736
+ throw new HpkeError(
1737
+ "ERR_INVALID_RECIPIENT_KEY",
1738
+ `${context} must be extractable (the KEM exports it)`
1739
+ );
1740
+ }
1741
+ return key;
1742
+ }
1743
+ __name(assertRecipientPublicKey, "assertRecipientPublicKey");
1714
1744
  function assertX25519Algorithm(key, context, code2) {
1715
1745
  if (key.algorithm.name !== "X25519") {
1716
1746
  throw new HpkeError(
@@ -1721,11 +1751,23 @@ function assertX25519Algorithm(key, context, code2) {
1721
1751
  }
1722
1752
  __name(assertX25519Algorithm, "assertX25519Algorithm");
1723
1753
  async function dh(priv, pub) {
1724
- const bits = await subtle2.deriveBits(
1725
- { name: "X25519", public: pub },
1726
- priv,
1727
- 256
1728
- );
1754
+ let bits;
1755
+ try {
1756
+ bits = await subtle2.deriveBits(
1757
+ { name: "X25519", public: pub },
1758
+ priv,
1759
+ 256
1760
+ );
1761
+ } catch (err) {
1762
+ if (err?.name === "OperationError") {
1763
+ throw new HpkeError(
1764
+ "ERR_SMALL_ORDER_KEY",
1765
+ "X25519 deriveBits rejected a small-order public key",
1766
+ { cause: err }
1767
+ );
1768
+ }
1769
+ throw err;
1770
+ }
1729
1771
  const bytes = new Uint8Array(bits);
1730
1772
  if (bytes.every((b) => b === 0)) {
1731
1773
  throw new HpkeError(
@@ -1778,6 +1820,23 @@ async function decap(enc, keypair) {
1778
1820
  "keypair.privateKey",
1779
1821
  "ERR_INVALID_KEYPAIR"
1780
1822
  );
1823
+ if (!keypair.privateKey.usages.includes("deriveBits")) {
1824
+ throw new HpkeError(
1825
+ "ERR_INVALID_KEYPAIR",
1826
+ "keypair.privateKey must have the deriveBits usage"
1827
+ );
1828
+ }
1829
+ if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
1830
+ throw new HpkeError(
1831
+ "ERR_INVALID_KEYPAIR",
1832
+ "keypair.publicKey must be an extractable public key"
1833
+ );
1834
+ }
1835
+ assertX25519Algorithm(
1836
+ keypair.publicKey,
1837
+ "keypair.publicKey",
1838
+ "ERR_INVALID_KEYPAIR"
1839
+ );
1781
1840
  const pkE = await subtle2.importKey(
1782
1841
  "raw",
1783
1842
  enc,
@@ -1820,6 +1879,16 @@ async function keyId(key) {
1820
1879
  return toString2(new Uint8Array(digest2), "base64url");
1821
1880
  }
1822
1881
  __name(keyId, "keyId");
1882
+ async function verifyKeyId(key, expected) {
1883
+ const actual = await keyId(key);
1884
+ if (actual.length !== expected.length) return false;
1885
+ let diff = 0;
1886
+ for (let i = 0; i < actual.length; i++) {
1887
+ diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
1888
+ }
1889
+ return diff === 0;
1890
+ }
1891
+ __name(verifyKeyId, "verifyKeyId");
1823
1892
  function i2osp(n, len) {
1824
1893
  const out = new Uint8Array(len);
1825
1894
  let v = n;
@@ -1855,6 +1924,7 @@ export {
1855
1924
  keyId,
1856
1925
  labeledExpand,
1857
1926
  labeledExtract,
1858
- resolveRecipientPublicKey
1927
+ resolveRecipientPublicKey,
1928
+ verifyKeyId
1859
1929
  };
1860
1930
  //# sourceMappingURL=util.js.map