simple-hpke 0.2.0 → 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 +131 -19
- package/dist/constants.cjs +116 -34
- 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 +116 -34
- 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 -36
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +31 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -36
- 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 +77 -18
- package/dist/util.cjs +144 -34
- package/dist/util.cjs.map +4 -4
- package/dist/util.d.ts +32 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +144 -34
- 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
|
@@ -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)
|
|
@@ -77,20 +80,24 @@ const keypair = await crypto.subtle.generateKey(
|
|
|
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
|
|
80
|
-
// `
|
|
81
|
-
const {
|
|
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).
|
|
82
89
|
|
|
83
90
|
//
|
|
84
91
|
// Or wrap an existing AES key. The supplied AES key must be extractable.
|
|
85
92
|
//
|
|
86
93
|
const aesKey = await crypto.subtle.generateKey(
|
|
87
94
|
{ name: 'AES-GCM', length: 256 },
|
|
88
|
-
true, //
|
|
95
|
+
true, // extractable -- its raw bytes are what get sealed
|
|
89
96
|
['encrypt', 'decrypt']
|
|
90
97
|
)
|
|
91
98
|
|
|
92
99
|
// Wrap the existing key.
|
|
93
|
-
const {
|
|
100
|
+
const { wrapped: wrappedKey } = await encryptKey(keypair, aesKey)
|
|
94
101
|
|
|
95
102
|
// or pass in just a public key
|
|
96
103
|
|
|
@@ -121,7 +128,7 @@ const recipient = await crypto.subtle.generateKey(
|
|
|
121
128
|
)
|
|
122
129
|
|
|
123
130
|
// Create a fresh AES-GCM key, and encrypt a message with it.
|
|
124
|
-
const {
|
|
131
|
+
const { wrapped, key } = await create(recipient)
|
|
125
132
|
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
126
133
|
const ciphertextBuffer = await crypto.subtle.encrypt(
|
|
127
134
|
{ name: 'AES-GCM', iv },
|
|
@@ -129,25 +136,28 @@ const ciphertextBuffer = await crypto.subtle.encrypt(
|
|
|
129
136
|
new TextEncoder().encode('attack at dawn')
|
|
130
137
|
)
|
|
131
138
|
|
|
132
|
-
// Send `
|
|
133
|
-
// 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.
|
|
134
141
|
//
|
|
135
|
-
// If your protocol allows different wrapped-key sizes, prefix
|
|
136
|
-
// (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.
|
|
137
145
|
const ciphertext = new Uint8Array(ciphertextBuffer)
|
|
138
|
-
const
|
|
139
|
-
const message = new Uint8Array(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
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)
|
|
143
153
|
|
|
144
154
|
// On the other side, split the payload back into its parts.
|
|
145
|
-
const
|
|
146
|
-
const iv2 = message.subarray(
|
|
147
|
-
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)
|
|
148
158
|
|
|
149
159
|
// Recover the key, then decrypt the message.
|
|
150
|
-
const recovered = await open(recipient,
|
|
160
|
+
const recovered = await open(recipient, wrapped2)
|
|
151
161
|
const plaintext = await crypto.subtle.decrypt(
|
|
152
162
|
{ name: 'AES-GCM', iv: iv2 },
|
|
153
163
|
recovered,
|
|
@@ -334,6 +344,100 @@ decrypt.fromString = async function decryptFromString (
|
|
|
334
344
|
|
|
335
345
|
---------------------------------------------------------------
|
|
336
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
|
+
|
|
337
441
|
## Modules
|
|
338
442
|
|
|
339
443
|
This exposes ESM and common JS via
|
|
@@ -341,7 +445,15 @@ This exposes ESM and common JS via
|
|
|
341
445
|
|
|
342
446
|
### ESM
|
|
343
447
|
```js
|
|
344
|
-
import {
|
|
448
|
+
import {
|
|
449
|
+
create,
|
|
450
|
+
encryptKey,
|
|
451
|
+
open,
|
|
452
|
+
encrypt,
|
|
453
|
+
decrypt,
|
|
454
|
+
keyId,
|
|
455
|
+
HpkeError
|
|
456
|
+
} from 'simple-hpke'
|
|
345
457
|
```
|
|
346
458
|
|
|
347
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,6 +1317,19 @@ 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
1335
|
var open = Object.assign(openBytes, {
|
|
@@ -1329,13 +1344,19 @@ var decrypt = Object.assign(decryptBytes, {
|
|
|
1329
1344
|
});
|
|
1330
1345
|
async function decryptBytes(keypair, message, opts) {
|
|
1331
1346
|
if (message.byteLength < WRAPPED_LEN_PREFIX) {
|
|
1332
|
-
throw new
|
|
1347
|
+
throw new HpkeError(
|
|
1348
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1349
|
+
"malformed message: too short"
|
|
1350
|
+
);
|
|
1333
1351
|
}
|
|
1334
1352
|
const wrappedLen = message[0] << 8 | message[1];
|
|
1335
1353
|
const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
|
|
1336
1354
|
const ctStart = ivStart + NN;
|
|
1337
1355
|
if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
|
|
1338
|
-
throw new
|
|
1356
|
+
throw new HpkeError(
|
|
1357
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1358
|
+
"malformed message: too short"
|
|
1359
|
+
);
|
|
1339
1360
|
}
|
|
1340
1361
|
const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
|
|
1341
1362
|
const iv = message.slice(ivStart, ctStart);
|
|
@@ -1346,11 +1367,20 @@ async function decryptBytes(keypair, message, opts) {
|
|
|
1346
1367
|
opts?.info !== void 0 ? { info: opts.info } : void 0
|
|
1347
1368
|
);
|
|
1348
1369
|
const key = await importAesKeyDecryptOnly(keyBytes);
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
+
}
|
|
1354
1384
|
return new Uint8Array(pt);
|
|
1355
1385
|
}
|
|
1356
1386
|
__name(decryptBytes, "decryptBytes");
|
|
@@ -1371,7 +1401,8 @@ async function decryptFromString(keypair, message, opts) {
|
|
|
1371
1401
|
__name(decryptFromString, "decryptFromString");
|
|
1372
1402
|
function validateKeysize(keysize) {
|
|
1373
1403
|
if (keysize !== 128 && keysize !== 256) {
|
|
1374
|
-
throw new
|
|
1404
|
+
throw new HpkeError(
|
|
1405
|
+
"ERR_INVALID_KEYSIZE",
|
|
1375
1406
|
`invalid keysize: ${keysize} (expected 128 or 256)`
|
|
1376
1407
|
);
|
|
1377
1408
|
}
|
|
@@ -1379,7 +1410,8 @@ function validateKeysize(keysize) {
|
|
|
1379
1410
|
__name(validateKeysize, "validateKeysize");
|
|
1380
1411
|
function validateRawKeyBytes(raw) {
|
|
1381
1412
|
if (raw.length !== 16 && raw.length !== 32) {
|
|
1382
|
-
throw new
|
|
1413
|
+
throw new HpkeError(
|
|
1414
|
+
"ERR_INVALID_AES_KEY",
|
|
1383
1415
|
`invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
|
|
1384
1416
|
);
|
|
1385
1417
|
}
|
|
@@ -1393,19 +1425,20 @@ function normalizeInfo(info) {
|
|
|
1393
1425
|
__name(normalizeInfo, "normalizeInfo");
|
|
1394
1426
|
async function exportAesKeyBytes(key) {
|
|
1395
1427
|
if (!key.extractable) {
|
|
1396
|
-
throw new
|
|
1428
|
+
throw new HpkeError(
|
|
1429
|
+
"ERR_INVALID_AES_KEY",
|
|
1397
1430
|
"aesKey must be extractable: its raw bytes are what get sealed"
|
|
1398
1431
|
);
|
|
1399
1432
|
}
|
|
1400
1433
|
return new Uint8Array(await subtle.exportKey("raw", key));
|
|
1401
1434
|
}
|
|
1402
1435
|
__name(exportAesKeyBytes, "exportAesKeyBytes");
|
|
1403
|
-
async function importAesKey(raw) {
|
|
1436
|
+
async function importAesKey(raw, extractable) {
|
|
1404
1437
|
return subtle.importKey(
|
|
1405
1438
|
"raw",
|
|
1406
1439
|
raw,
|
|
1407
1440
|
{ name: "AES-GCM" },
|
|
1408
|
-
|
|
1441
|
+
extractable,
|
|
1409
1442
|
["encrypt", "decrypt"]
|
|
1410
1443
|
);
|
|
1411
1444
|
}
|
|
@@ -1460,7 +1493,10 @@ async function keySchedule(sharedSecret, info) {
|
|
|
1460
1493
|
__name(keySchedule, "keySchedule");
|
|
1461
1494
|
async function openRawBytes(keypair, wrapped, opts) {
|
|
1462
1495
|
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1463
|
-
throw new
|
|
1496
|
+
throw new HpkeError(
|
|
1497
|
+
"ERR_MALFORMED_ENVELOPE",
|
|
1498
|
+
"malformed envelope: too short"
|
|
1499
|
+
);
|
|
1464
1500
|
}
|
|
1465
1501
|
const info = normalizeInfo(opts?.info);
|
|
1466
1502
|
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
@@ -1472,7 +1508,7 @@ async function openRawBytes(keypair, wrapped, opts) {
|
|
|
1472
1508
|
__name(openRawBytes, "openRawBytes");
|
|
1473
1509
|
async function openBytes(keypair, wrapped, opts) {
|
|
1474
1510
|
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1475
|
-
return importAesKey(keyBytes);
|
|
1511
|
+
return importAesKey(keyBytes, opts?.extractable ?? false);
|
|
1476
1512
|
}
|
|
1477
1513
|
__name(openBytes, "openBytes");
|
|
1478
1514
|
async function encryptKey(recipient, aesKey, opts) {
|
|
@@ -1496,8 +1532,11 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1496
1532
|
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1497
1533
|
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1498
1534
|
const wrapped = concat2(enc, ciphertext);
|
|
1499
|
-
const aesGcmKey = await importAesKey(
|
|
1500
|
-
|
|
1535
|
+
const aesGcmKey = await importAesKey(
|
|
1536
|
+
keyBytes,
|
|
1537
|
+
opts?.extractable ?? false
|
|
1538
|
+
);
|
|
1539
|
+
return { wrapped, key: aesGcmKey };
|
|
1501
1540
|
}
|
|
1502
1541
|
__name(encryptKey, "encryptKey");
|
|
1503
1542
|
|
|
@@ -1527,24 +1566,38 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1527
1566
|
false,
|
|
1528
1567
|
["decrypt"]
|
|
1529
1568
|
);
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
+
}
|
|
1535
1583
|
return new Uint8Array(pt);
|
|
1536
1584
|
}
|
|
1537
1585
|
__name(aeadOpen, "aeadOpen");
|
|
1538
1586
|
async function encryptBytes(recipient, message, aesKey, opts) {
|
|
1539
1587
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1540
|
-
const {
|
|
1588
|
+
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1541
1589
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
1542
1590
|
const ct = new Uint8Array(await subtle2.encrypt(
|
|
1543
1591
|
{ name: "AES-GCM", iv },
|
|
1544
1592
|
key,
|
|
1545
1593
|
plaintext
|
|
1546
1594
|
));
|
|
1547
|
-
return concat2(
|
|
1595
|
+
return concat2(
|
|
1596
|
+
i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
|
|
1597
|
+
wrapped,
|
|
1598
|
+
iv,
|
|
1599
|
+
ct
|
|
1600
|
+
);
|
|
1548
1601
|
}
|
|
1549
1602
|
__name(encryptBytes, "encryptBytes");
|
|
1550
1603
|
async function encryptToString(recipient, message, aesKey, opts) {
|
|
@@ -1610,7 +1663,8 @@ async function exportRawPublic(key) {
|
|
|
1610
1663
|
__name(exportRawPublic, "exportRawPublic");
|
|
1611
1664
|
async function importRawPublic(raw) {
|
|
1612
1665
|
if (raw.length !== ENC_LENGTH) {
|
|
1613
|
-
throw new
|
|
1666
|
+
throw new HpkeError(
|
|
1667
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1614
1668
|
`invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
|
|
1615
1669
|
);
|
|
1616
1670
|
}
|
|
@@ -1629,9 +1683,16 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1629
1683
|
}
|
|
1630
1684
|
if (recipient instanceof CryptoKey) {
|
|
1631
1685
|
if (recipient.type !== "public") {
|
|
1632
|
-
throw new
|
|
1686
|
+
throw new HpkeError(
|
|
1687
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1688
|
+
"recipient CryptoKey must be a public key"
|
|
1689
|
+
);
|
|
1633
1690
|
}
|
|
1634
|
-
assertX25519Algorithm(
|
|
1691
|
+
assertX25519Algorithm(
|
|
1692
|
+
recipient,
|
|
1693
|
+
"recipient CryptoKey",
|
|
1694
|
+
"ERR_INVALID_RECIPIENT_KEY"
|
|
1695
|
+
);
|
|
1635
1696
|
return recipient;
|
|
1636
1697
|
}
|
|
1637
1698
|
if (typeof recipient.publicKey === "string") {
|
|
@@ -1641,17 +1702,28 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1641
1702
|
const pair = recipient;
|
|
1642
1703
|
if (pair.publicKey instanceof CryptoKey) {
|
|
1643
1704
|
if (pair.publicKey.type !== "public") {
|
|
1644
|
-
throw new
|
|
1705
|
+
throw new HpkeError(
|
|
1706
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1707
|
+
"recipient keypair publicKey must be a public key"
|
|
1708
|
+
);
|
|
1645
1709
|
}
|
|
1646
|
-
assertX25519Algorithm(
|
|
1710
|
+
assertX25519Algorithm(
|
|
1711
|
+
pair.publicKey,
|
|
1712
|
+
"recipient keypair publicKey",
|
|
1713
|
+
"ERR_INVALID_RECIPIENT_KEY"
|
|
1714
|
+
);
|
|
1647
1715
|
return pair.publicKey;
|
|
1648
1716
|
}
|
|
1649
|
-
throw new
|
|
1717
|
+
throw new HpkeError(
|
|
1718
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1719
|
+
"unrecognized recipient key form"
|
|
1720
|
+
);
|
|
1650
1721
|
}
|
|
1651
1722
|
__name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
|
|
1652
|
-
function assertX25519Algorithm(key, context) {
|
|
1723
|
+
function assertX25519Algorithm(key, context, code2) {
|
|
1653
1724
|
if (key.algorithm.name !== "X25519") {
|
|
1654
|
-
throw new
|
|
1725
|
+
throw new HpkeError(
|
|
1726
|
+
code2,
|
|
1655
1727
|
`${context} must be an X25519 key (got ${key.algorithm.name})`
|
|
1656
1728
|
);
|
|
1657
1729
|
}
|
|
@@ -1665,7 +1737,8 @@ async function dh(priv, pub) {
|
|
|
1665
1737
|
);
|
|
1666
1738
|
const bytes = new Uint8Array(bits);
|
|
1667
1739
|
if (bytes.every((b) => b === 0)) {
|
|
1668
|
-
throw new
|
|
1740
|
+
throw new HpkeError(
|
|
1741
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1669
1742
|
"X25519 shared secret is all-zero (small-order public key?)"
|
|
1670
1743
|
);
|
|
1671
1744
|
}
|
|
@@ -1704,9 +1777,16 @@ async function encap(pkR) {
|
|
|
1704
1777
|
__name(encap, "encap");
|
|
1705
1778
|
async function decap(enc, keypair) {
|
|
1706
1779
|
if (keypair.privateKey.type !== "private") {
|
|
1707
|
-
throw new
|
|
1780
|
+
throw new HpkeError(
|
|
1781
|
+
"ERR_INVALID_KEYPAIR",
|
|
1782
|
+
"keypair.privateKey must be a private key"
|
|
1783
|
+
);
|
|
1708
1784
|
}
|
|
1709
|
-
assertX25519Algorithm(
|
|
1785
|
+
assertX25519Algorithm(
|
|
1786
|
+
keypair.privateKey,
|
|
1787
|
+
"keypair.privateKey",
|
|
1788
|
+
"ERR_INVALID_KEYPAIR"
|
|
1789
|
+
);
|
|
1710
1790
|
const pkE = await subtle2.importKey(
|
|
1711
1791
|
"raw",
|
|
1712
1792
|
enc,
|
|
@@ -1765,4 +1845,6 @@ var HPKE_SUITE_ID = concat2(
|
|
|
1765
1845
|
i2osp(KDF_ID, 2),
|
|
1766
1846
|
i2osp(AEAD_ID, 2)
|
|
1767
1847
|
);
|
|
1848
|
+
var KEY_ID_LABEL = new TextEncoder().encode("simple-hpke/keyId/v1");
|
|
1849
|
+
var KEY_ID_NONCE_LENGTH = 12;
|
|
1768
1850
|
//# sourceMappingURL=constants.cjs.map
|