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/README.md +82 -14
- package/dist/constants.cjs +87 -28
- package/dist/constants.cjs.map +2 -2
- package/dist/constants.js +87 -28
- package/dist/constants.js.map +2 -2
- package/dist/constants.min.cjs +1 -1
- package/dist/constants.min.cjs.map +3 -3
- package/dist/constants.min.js +1 -1
- package/dist/constants.min.js.map +3 -3
- package/dist/index.cjs +99 -29
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -29
- package/dist/index.js.map +2 -2
- package/dist/index.min.cjs +1 -1
- package/dist/index.min.cjs.map +3 -3
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +3 -3
- package/dist/meta.json +18 -16
- package/dist/util.cjs +99 -29
- package/dist/util.cjs.map +2 -2
- package/dist/util.d.ts +24 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +99 -29
- package/dist/util.js.map +2 -2
- package/dist/util.min.cjs +1 -1
- package/dist/util.min.cjs.map +3 -3
- package/dist/util.min.js +1 -1
- package/dist/util.min.js.map +3 -3
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ the plaintext is.
|
|
|
31
31
|
- [Install](#install)
|
|
32
32
|
- [Examples](#examples)
|
|
33
33
|
* [Key Wrapping](#key-wrapping)
|
|
34
|
+
+ [`open.raw`](#openraw)
|
|
34
35
|
* [Hybrid Encryption](#hybrid-encryption)
|
|
35
36
|
+ [Encrypt / Decrypt](#encrypt--decrypt)
|
|
36
37
|
- [`encrypt`](#encrypt)
|
|
@@ -40,6 +41,7 @@ the plaintext is.
|
|
|
40
41
|
- [`decrypt.fromString`](#decryptfromstring)
|
|
41
42
|
- [Extractable keys](#extractable-keys)
|
|
42
43
|
- [`keyId`](#keyid)
|
|
44
|
+
* [`verifyKeyId`](#verifykeyid)
|
|
43
45
|
- [Errors](#errors)
|
|
44
46
|
- [Modules](#modules)
|
|
45
47
|
* [ESM](#esm)
|
|
@@ -96,15 +98,29 @@ const aesKey = await crypto.subtle.generateKey(
|
|
|
96
98
|
['encrypt', 'decrypt']
|
|
97
99
|
)
|
|
98
100
|
|
|
99
|
-
// Wrap the existing key.
|
|
100
|
-
|
|
101
|
+
// Wrap the existing key. The returned key is your own `aesKey` object,
|
|
102
|
+
// untouched (`sameKey === aesKey`): same `extractable`, same usages.
|
|
103
|
+
const { wrapped: wrappedKey, key: sameKey } = await encryptKey(
|
|
104
|
+
keypair,
|
|
105
|
+
aesKey
|
|
106
|
+
)
|
|
101
107
|
|
|
102
108
|
// or pass in just a public key
|
|
103
109
|
|
|
104
110
|
// Later, recover the same key with your private key.
|
|
105
111
|
const recoveredKey = await open(keypair, wrappedKey)
|
|
106
112
|
|
|
107
|
-
// `recoveredKey`
|
|
113
|
+
// `recoveredKey` holds the same key material as `aesKey`
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### `open.raw`
|
|
117
|
+
|
|
118
|
+
Recover the wrapped key as raw bytes instead of a `CryptoKey`. Returns a
|
|
119
|
+
`Uint8Array` of 16 or 32 bytes, matching the key that was wrapped. It
|
|
120
|
+
takes the same `info` option as `open`, and no `extractable` option.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const keyBytes = await open.raw(keypair, wrappedKey)
|
|
108
124
|
```
|
|
109
125
|
|
|
110
126
|
|
|
@@ -114,7 +130,7 @@ const recoveredKey = await open(keypair, wrappedKey)
|
|
|
114
130
|
### Hybrid Encryption
|
|
115
131
|
|
|
116
132
|
Encrypt a message with AES-GCM, then encrypt the AES key to a given
|
|
117
|
-
public key. The encrypted key is
|
|
133
|
+
public key. The encrypted key is concatenated with the cipher text,
|
|
118
134
|
along with the IV. The recipient uses their private key to open the AES key and
|
|
119
135
|
decrypt the message.
|
|
120
136
|
|
|
@@ -209,7 +225,7 @@ const encryptedMessage = await encrypt(recipient.publicKey, 'hello encryption')
|
|
|
209
225
|
|
|
210
226
|
// encrypt to a public key as Uint8Array
|
|
211
227
|
const publicBytes = new Uint8Array(
|
|
212
|
-
await subtle.exportKey('raw', recipient.publicKey)
|
|
228
|
+
await crypto.subtle.exportKey('raw', recipient.publicKey)
|
|
213
229
|
)
|
|
214
230
|
const envelope = await encrypt(publicBytes, 'hello again')
|
|
215
231
|
|
|
@@ -256,14 +272,16 @@ const plaintext = await decrypt.asString(recipient, envelope)
|
|
|
256
272
|
|
|
257
273
|
##### `encrypt`
|
|
258
274
|
|
|
259
|
-
The recipient can be a crypto key, a
|
|
275
|
+
The recipient can be a public crypto key, a keypair (only its public key
|
|
276
|
+
is used), a Uint8Array, or a string public key. `SupportedEncodings` is
|
|
277
|
+
the encoding type from `uint8arrays`.
|
|
260
278
|
|
|
261
279
|
```ts
|
|
262
280
|
type RecipientKey =
|
|
263
281
|
| CryptoKey
|
|
264
282
|
| CryptoKeyPair
|
|
265
283
|
| Uint8Array
|
|
266
|
-
| { publicKey:string; encoding?:
|
|
284
|
+
| { publicKey:string; encoding?:SupportedEncodings }
|
|
267
285
|
|
|
268
286
|
async function encrypt (
|
|
269
287
|
recipient:RecipientKey,
|
|
@@ -276,6 +294,11 @@ async function encrypt (
|
|
|
276
294
|
):Promise<Uint8Array>
|
|
277
295
|
```
|
|
278
296
|
|
|
297
|
+
A supplied `aesKey` `CryptoKey` must be extractable and must have the
|
|
298
|
+
`encrypt` usage, because the message is encrypted under that key directly.
|
|
299
|
+
A key without it rejects with `ERR_INVALID_AES_KEY` before any work is
|
|
300
|
+
done. The key is used as-is, never re-imported.
|
|
301
|
+
|
|
279
302
|
##### `encrypt.asString`
|
|
280
303
|
|
|
281
304
|
`encrypt.asString` is `encrypt` with the envelope encoded to a string, useful
|
|
@@ -331,8 +354,9 @@ decrypt.asString = async function decryptToString (
|
|
|
331
354
|
|
|
332
355
|
##### `decrypt.fromString`
|
|
333
356
|
|
|
334
|
-
Take a `
|
|
335
|
-
a`
|
|
357
|
+
Take a `base64url` string as input (as returned by `encrypt.asString` with
|
|
358
|
+
the default encoding). Return either a string, or if `opts.buffer` is true,
|
|
359
|
+
a `Uint8Array`.
|
|
336
360
|
|
|
337
361
|
```ts
|
|
338
362
|
decrypt.fromString = async function decryptFromString (
|
|
@@ -364,6 +388,23 @@ This is separate from the `aesKey` you may pass **in** to `encryptKey` /
|
|
|
364
388
|
`encrypt`. That one must always be extractable, because its raw bytes are
|
|
365
389
|
what get sealed.
|
|
366
390
|
|
|
391
|
+
A supplied `CryptoKey` is returned **as-is**: `encryptKey` gives back the
|
|
392
|
+
same object you passed in, with its own `extractable` flag and usages.
|
|
393
|
+
`opts.extractable` does not apply to it; it only governs a key the library
|
|
394
|
+
imports (a generated key, or raw `Uint8Array` bytes). Because the library
|
|
395
|
+
does not re-import your key, it does not add usages either. `encryptKey`
|
|
396
|
+
will wrap a decrypt-only key, but `encrypt` needs the `encrypt` usage.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
const myKey = await crypto.subtle.generateKey(
|
|
400
|
+
{ name: 'AES-GCM', length: 256 },
|
|
401
|
+
true,
|
|
402
|
+
['encrypt', 'decrypt']
|
|
403
|
+
)
|
|
404
|
+
const { key } = await encryptKey(recipient, myKey, { extractable: false })
|
|
405
|
+
key === myKey // true, and still extractable
|
|
406
|
+
```
|
|
407
|
+
|
|
367
408
|
If you only need to tell two keys apart, you do not need the bytes --
|
|
368
409
|
use [`keyId`](#keyid).
|
|
369
410
|
|
|
@@ -403,6 +444,32 @@ keyId = base64url(SHA-256(LABEL || C))
|
|
|
403
444
|
`key` must be an AES-GCM key with the `encrypt` usage; anything else
|
|
404
445
|
throws [`ERR_INVALID_AES_KEY`](#errors).
|
|
405
446
|
|
|
447
|
+
### `verifyKeyId`
|
|
448
|
+
|
|
449
|
+
Check a key against a `keyId` you stored earlier.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
async function verifyKeyId (
|
|
453
|
+
key:CryptoKey,
|
|
454
|
+
expected:string
|
|
455
|
+
):Promise<boolean>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { open, verifyKeyId } from 'simple-hpke'
|
|
460
|
+
|
|
461
|
+
const recovered = await open(keypair, wrapped)
|
|
462
|
+
if (!(await verifyKeyId(recovered, storedId))) {
|
|
463
|
+
// not the key you expected
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
It resolves `true` or `false`, comparing in constant time. A key that
|
|
468
|
+
`keyId` cannot fingerprint (not AES-GCM, or no `encrypt` usage) rejects
|
|
469
|
+
with [`ERR_INVALID_AES_KEY`](#errors) rather than resolving `false`, so
|
|
470
|
+
"cannot check this key" is never confused with "wrong key". Do not catch
|
|
471
|
+
that error and return `false`.
|
|
472
|
+
|
|
406
473
|
## Errors
|
|
407
474
|
|
|
408
475
|
Every error this package throws is an `HpkeError`: an ordinary `Error`
|
|
@@ -425,13 +492,13 @@ try {
|
|
|
425
492
|
|
|
426
493
|
| Code | Raised when |
|
|
427
494
|
| --- | --- |
|
|
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` |
|
|
495
|
+
| `ERR_INVALID_RECIPIENT_KEY` | The recipient is not a usable X25519 public key: wrong length, a private key, the wrong algorithm, a non-extractable public key, a string that does not decode, or an unrecognized form. |
|
|
496
|
+
| `ERR_INVALID_KEYPAIR` | `keypair.privateKey` is not an X25519 private key or lacks the `deriveBits` usage, or `keypair.publicKey` is not an extractable X25519 public key. |
|
|
497
|
+
| `ERR_SMALL_ORDER_KEY` | A small-order X25519 public key (for example all zeros), as the recipient or as the `enc` in a tampered envelope. Raised on conforming runtimes, which reject it inside `deriveBits`, and on nonconforming ones, where the shared secret comes out all-zero. |
|
|
431
498
|
| `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. |
|
|
499
|
+
| `ERR_MALFORMED_MESSAGE` | A `decrypt` envelope is too short to hold its own declared segments, or a `decrypt.fromString` input is not valid `base64url`. |
|
|
433
500
|
| `ERR_INVALID_KEYSIZE` | `opts.size` is neither `128` nor `256`. |
|
|
434
|
-
| `ERR_INVALID_AES_KEY` | A supplied `aesKey` is the wrong length
|
|
501
|
+
| `ERR_INVALID_AES_KEY` | A supplied `aesKey` is the wrong length, non-extractable, or not an AES-GCM key, `encrypt` got an `aesKey` without the `encrypt` usage, or `keyId` / `verifyKeyId` got a key it cannot use. |
|
|
435
502
|
| `ERR_DECRYPT_FAILED` | AES-GCM authentication failed: wrong keypair, wrong `info`, or a tampered ciphertext. |
|
|
436
503
|
|
|
437
504
|
`HpkeErrorCode` is exported as a type if you want to switch exhaustively.
|
|
@@ -452,6 +519,7 @@ import {
|
|
|
452
519
|
encrypt,
|
|
453
520
|
decrypt,
|
|
454
521
|
keyId,
|
|
522
|
+
verifyKeyId,
|
|
455
523
|
HpkeError
|
|
456
524
|
} from 'simple-hpke'
|
|
457
525
|
```
|
package/dist/constants.cjs
CHANGED
|
@@ -1390,7 +1390,16 @@ async function decryptToString(keypair, message, opts) {
|
|
|
1390
1390
|
}
|
|
1391
1391
|
__name(decryptToString, "decryptToString");
|
|
1392
1392
|
async function decryptFromString(keypair, message, opts) {
|
|
1393
|
-
|
|
1393
|
+
let bytes;
|
|
1394
|
+
try {
|
|
1395
|
+
bytes = fromString2(message, "base64url");
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
throw new HpkeError(
|
|
1398
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1399
|
+
"malformed message: not base64url",
|
|
1400
|
+
{ cause: err }
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1394
1403
|
const plaintext = await decryptBytes(
|
|
1395
1404
|
keypair,
|
|
1396
1405
|
bytes,
|
|
@@ -1518,6 +1527,12 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1518
1527
|
validateRawKeyBytes(aesKey);
|
|
1519
1528
|
keyBytes = aesKey;
|
|
1520
1529
|
} else if (aesKey) {
|
|
1530
|
+
if (aesKey.algorithm.name !== "AES-GCM") {
|
|
1531
|
+
throw new HpkeError(
|
|
1532
|
+
"ERR_INVALID_AES_KEY",
|
|
1533
|
+
"aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1521
1536
|
keyBytes = await exportAesKeyBytes(aesKey);
|
|
1522
1537
|
validateRawKeyBytes(keyBytes);
|
|
1523
1538
|
} else {
|
|
@@ -1532,6 +1547,7 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1532
1547
|
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1533
1548
|
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1534
1549
|
const wrapped = concat2(enc, ciphertext);
|
|
1550
|
+
if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
|
|
1535
1551
|
const aesGcmKey = await importAesKey(
|
|
1536
1552
|
keyBytes,
|
|
1537
1553
|
opts?.extractable ?? false
|
|
@@ -1584,6 +1600,12 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1584
1600
|
}
|
|
1585
1601
|
__name(aeadOpen, "aeadOpen");
|
|
1586
1602
|
async function encryptBytes(recipient, message, aesKey, opts) {
|
|
1603
|
+
if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
|
|
1604
|
+
throw new HpkeError(
|
|
1605
|
+
"ERR_INVALID_AES_KEY",
|
|
1606
|
+
'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1587
1609
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1588
1610
|
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1589
1611
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
@@ -1682,37 +1704,28 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1682
1704
|
return importRawPublic(recipient);
|
|
1683
1705
|
}
|
|
1684
1706
|
if (recipient instanceof CryptoKey) {
|
|
1685
|
-
|
|
1686
|
-
throw new HpkeError(
|
|
1687
|
-
"ERR_INVALID_RECIPIENT_KEY",
|
|
1688
|
-
"recipient CryptoKey must be a public key"
|
|
1689
|
-
);
|
|
1690
|
-
}
|
|
1691
|
-
assertX25519Algorithm(
|
|
1692
|
-
recipient,
|
|
1693
|
-
"recipient CryptoKey",
|
|
1694
|
-
"ERR_INVALID_RECIPIENT_KEY"
|
|
1695
|
-
);
|
|
1696
|
-
return recipient;
|
|
1707
|
+
return assertRecipientPublicKey(recipient, "recipient CryptoKey");
|
|
1697
1708
|
}
|
|
1698
1709
|
if (typeof recipient.publicKey === "string") {
|
|
1699
1710
|
const { publicKey, encoding } = recipient;
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
if (pair.publicKey.type !== "public") {
|
|
1711
|
+
let decoded;
|
|
1712
|
+
try {
|
|
1713
|
+
decoded = fromString2(publicKey, encoding ?? "base64url");
|
|
1714
|
+
} catch (err) {
|
|
1705
1715
|
throw new HpkeError(
|
|
1706
1716
|
"ERR_INVALID_RECIPIENT_KEY",
|
|
1707
|
-
|
|
1717
|
+
`recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
|
|
1718
|
+
{ cause: err }
|
|
1708
1719
|
);
|
|
1709
1720
|
}
|
|
1710
|
-
|
|
1721
|
+
return importRawPublic(decoded);
|
|
1722
|
+
}
|
|
1723
|
+
const pair = recipient;
|
|
1724
|
+
if (pair.publicKey instanceof CryptoKey) {
|
|
1725
|
+
return assertRecipientPublicKey(
|
|
1711
1726
|
pair.publicKey,
|
|
1712
|
-
"recipient keypair publicKey"
|
|
1713
|
-
"ERR_INVALID_RECIPIENT_KEY"
|
|
1727
|
+
"recipient keypair publicKey"
|
|
1714
1728
|
);
|
|
1715
|
-
return pair.publicKey;
|
|
1716
1729
|
}
|
|
1717
1730
|
throw new HpkeError(
|
|
1718
1731
|
"ERR_INVALID_RECIPIENT_KEY",
|
|
@@ -1720,6 +1733,23 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1720
1733
|
);
|
|
1721
1734
|
}
|
|
1722
1735
|
__name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
|
|
1736
|
+
function assertRecipientPublicKey(key, context) {
|
|
1737
|
+
if (key.type !== "public") {
|
|
1738
|
+
throw new HpkeError(
|
|
1739
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1740
|
+
`${context} must be a public key`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
|
|
1744
|
+
if (!key.extractable) {
|
|
1745
|
+
throw new HpkeError(
|
|
1746
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1747
|
+
`${context} must be extractable (the KEM exports it)`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
return key;
|
|
1751
|
+
}
|
|
1752
|
+
__name(assertRecipientPublicKey, "assertRecipientPublicKey");
|
|
1723
1753
|
function assertX25519Algorithm(key, context, code2) {
|
|
1724
1754
|
if (key.algorithm.name !== "X25519") {
|
|
1725
1755
|
throw new HpkeError(
|
|
@@ -1730,11 +1760,23 @@ function assertX25519Algorithm(key, context, code2) {
|
|
|
1730
1760
|
}
|
|
1731
1761
|
__name(assertX25519Algorithm, "assertX25519Algorithm");
|
|
1732
1762
|
async function dh(priv, pub) {
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1763
|
+
let bits;
|
|
1764
|
+
try {
|
|
1765
|
+
bits = await subtle2.deriveBits(
|
|
1766
|
+
{ name: "X25519", public: pub },
|
|
1767
|
+
priv,
|
|
1768
|
+
256
|
|
1769
|
+
);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
if (err?.name === "OperationError") {
|
|
1772
|
+
throw new HpkeError(
|
|
1773
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1774
|
+
"X25519 deriveBits rejected a small-order public key",
|
|
1775
|
+
{ cause: err }
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
throw err;
|
|
1779
|
+
}
|
|
1738
1780
|
const bytes = new Uint8Array(bits);
|
|
1739
1781
|
if (bytes.every((b) => b === 0)) {
|
|
1740
1782
|
throw new HpkeError(
|
|
@@ -1787,6 +1829,23 @@ async function decap(enc, keypair) {
|
|
|
1787
1829
|
"keypair.privateKey",
|
|
1788
1830
|
"ERR_INVALID_KEYPAIR"
|
|
1789
1831
|
);
|
|
1832
|
+
if (!keypair.privateKey.usages.includes("deriveBits")) {
|
|
1833
|
+
throw new HpkeError(
|
|
1834
|
+
"ERR_INVALID_KEYPAIR",
|
|
1835
|
+
"keypair.privateKey must have the deriveBits usage"
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
|
|
1839
|
+
throw new HpkeError(
|
|
1840
|
+
"ERR_INVALID_KEYPAIR",
|
|
1841
|
+
"keypair.publicKey must be an extractable public key"
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
assertX25519Algorithm(
|
|
1845
|
+
keypair.publicKey,
|
|
1846
|
+
"keypair.publicKey",
|
|
1847
|
+
"ERR_INVALID_KEYPAIR"
|
|
1848
|
+
);
|
|
1790
1849
|
const pkE = await subtle2.importKey(
|
|
1791
1850
|
"raw",
|
|
1792
1851
|
enc,
|