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 +137 -23
- package/dist/constants.cjs +115 -53
- package/dist/constants.cjs.map +4 -4
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +115 -53
- package/dist/constants.js.map +4 -4
- package/dist/constants.min.cjs +1 -1
- package/dist/constants.min.cjs.map +4 -4
- package/dist/constants.min.js +1 -1
- package/dist/constants.min.js.map +4 -4
- package/dist/errors.cjs +38 -0
- package/dist/errors.cjs.map +7 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +7 -0
- package/dist/errors.min.cjs +2 -0
- package/dist/errors.min.cjs.map +7 -0
- package/dist/errors.min.js +2 -0
- package/dist/errors.min.js.map +7 -0
- package/dist/index.cjs +147 -51
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +53 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -51
- package/dist/index.js.map +4 -4
- package/dist/index.min.cjs +1 -1
- package/dist/index.min.cjs.map +4 -4
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +4 -4
- package/dist/meta.json +79 -20
- package/dist/util.cjs +143 -53
- package/dist/util.cjs.map +4 -4
- package/dist/util.d.ts +33 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +143 -53
- package/dist/util.js.map +4 -4
- package/dist/util.min.cjs +1 -1
- package/dist/util.min.cjs.map +4 -4
- package/dist/util.min.js +1 -1
- package/dist/util.min.js.map +4 -4
- package/package.json +2 -2
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`/`
|
|
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,
|
|
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
|
-
|
|
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, //
|
|
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 {
|
|
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,
|
|
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
|
|
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 {
|
|
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 `
|
|
131
|
-
// 256-bit AES key, so `
|
|
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
|
|
134
|
-
// (or otherwise transmit it) so the recipient can split
|
|
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
|
|
137
|
-
const message = new Uint8Array(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
message.set(
|
|
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
|
|
144
|
-
const iv2 = message.subarray(
|
|
145
|
-
const ciphertext2 = message.subarray(
|
|
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,
|
|
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`
|
|
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 {
|
|
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
|
package/dist/constants.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
1686
|
+
throw new HpkeError(
|
|
1687
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1688
|
+
"recipient CryptoKey must be a public key"
|
|
1689
|
+
);
|
|
1653
1690
|
}
|
|
1654
|
-
assertX25519Algorithm(
|
|
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
|
|
1705
|
+
throw new HpkeError(
|
|
1706
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1707
|
+
"recipient keypair publicKey must be a public key"
|
|
1708
|
+
);
|
|
1665
1709
|
}
|
|
1666
|
-
assertX25519Algorithm(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1780
|
+
throw new HpkeError(
|
|
1781
|
+
"ERR_INVALID_KEYPAIR",
|
|
1782
|
+
"keypair.privateKey must be a private key"
|
|
1783
|
+
);
|
|
1728
1784
|
}
|
|
1729
|
-
assertX25519Algorithm(
|
|
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
|