simple-hpke 0.2.0 → 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 +207 -27
- package/dist/constants.cjs +188 -47
- 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 +188 -47
- 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 +231 -50
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +34 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +231 -50
- 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 +81 -20
- package/dist/util.cjs +228 -48
- package/dist/util.cjs.map +4 -4
- package/dist/util.d.ts +56 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +228 -48
- 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 +6 -5
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
|
|
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
|
|
@@ -49,7 +53,7 @@ export declare function encryptBytes(recipient: RecipientKey, message: Uint8Arra
|
|
|
49
53
|
info?: Uint8Array | string;
|
|
50
54
|
}): Promise<Uint8Array>;
|
|
51
55
|
/**
|
|
52
|
-
* Like `encrypt`, but encodes the envelope bytes to a string
|
|
56
|
+
* Like `encrypt`, but encodes the envelope bytes to a string -- handy for
|
|
53
57
|
* transports that carry text (JSON, URLs, headers). Decode with
|
|
54
58
|
* `fromString(...)` (or any matching decoder) and pass the bytes to
|
|
55
59
|
* `decrypt` / `decrypt.asString`. Exposed as `encrypt.asString`.
|
|
@@ -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
|
|
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
|
|
@@ -85,6 +93,51 @@ export declare function encap(pkR: CryptoKey): Promise<{
|
|
|
85
93
|
enc: Uint8Array;
|
|
86
94
|
}>;
|
|
87
95
|
export declare function decap(enc: Uint8Array, keypair: CryptoKeyPair): Promise<Uint8Array>;
|
|
96
|
+
/**
|
|
97
|
+
* A stable fingerprint for an AES-GCM key: the same key bytes always give
|
|
98
|
+
* the same string, and two different keys effectively never collide.
|
|
99
|
+
*
|
|
100
|
+
* Derived from the key's *behavior* rather than its bytes, so it works on
|
|
101
|
+
* a non-extractable key -- including one rehydrated from IndexedDB, and
|
|
102
|
+
* the keys `create` / `encryptKey` / `open` return by default. The obvious
|
|
103
|
+
* implementation, `exportKey('raw')` then SHA-256, would force every such
|
|
104
|
+
* key to be extractable. HKDF is not an option either: WebCrypto cannot
|
|
105
|
+
* convert an AES-GCM key into an HKDF key, and `deriveBits` only accepts a
|
|
106
|
+
* key imported under HKDF, which needs the raw bytes a non-extractable key
|
|
107
|
+
* withholds. AES-GCM as a PRF is the only primitive such a key still
|
|
108
|
+
* exposes.
|
|
109
|
+
*
|
|
110
|
+
* The derivation is fixed forever (see `src/constants.ts`):
|
|
111
|
+
*
|
|
112
|
+
* ```
|
|
113
|
+
* LABEL = utf8("simple-hpke/keyId/v1")
|
|
114
|
+
* N = SHA-256(LABEL)[0..12] // fixed nonce
|
|
115
|
+
* C = AES-GCM(key, N, LABEL) // ciphertext || 16-byte tag
|
|
116
|
+
* keyId = base64url(SHA-256(LABEL || C))
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @param key An AES-GCM `CryptoKey` with the `encrypt` usage. It does not
|
|
120
|
+
* need to be extractable.
|
|
121
|
+
* @returns The fingerprint: the full 32-byte digest, base64url, unpadded
|
|
122
|
+
* (43 characters).
|
|
123
|
+
* @throws {HpkeError} `ERR_INVALID_AES_KEY` if `key` is not an AES-GCM key
|
|
124
|
+
* or cannot encrypt.
|
|
125
|
+
*/
|
|
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>;
|
|
88
141
|
export declare function i2osp(n: number, len: number): Uint8Array;
|
|
89
142
|
export declare function concat(...arrays: Uint8Array[]): Uint8Array;
|
|
90
143
|
//# sourceMappingURL=util.d.ts.map
|
package/dist/util.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
|
package/dist/util.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/index.ts
|
|
1310
1325
|
var subtle = globalThis.crypto.subtle;
|
|
@@ -1320,13 +1335,19 @@ var decrypt = Object.assign(decryptBytes, {
|
|
|
1320
1335
|
});
|
|
1321
1336
|
async function decryptBytes(keypair, message, opts) {
|
|
1322
1337
|
if (message.byteLength < WRAPPED_LEN_PREFIX) {
|
|
1323
|
-
throw new
|
|
1338
|
+
throw new HpkeError(
|
|
1339
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1340
|
+
"malformed message: too short"
|
|
1341
|
+
);
|
|
1324
1342
|
}
|
|
1325
1343
|
const wrappedLen = message[0] << 8 | message[1];
|
|
1326
1344
|
const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
|
|
1327
1345
|
const ctStart = ivStart + NN;
|
|
1328
1346
|
if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
|
|
1329
|
-
throw new
|
|
1347
|
+
throw new HpkeError(
|
|
1348
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1349
|
+
"malformed message: too short"
|
|
1350
|
+
);
|
|
1330
1351
|
}
|
|
1331
1352
|
const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
|
|
1332
1353
|
const iv = message.slice(ivStart, ctStart);
|
|
@@ -1337,11 +1358,20 @@ async function decryptBytes(keypair, message, opts) {
|
|
|
1337
1358
|
opts?.info !== void 0 ? { info: opts.info } : void 0
|
|
1338
1359
|
);
|
|
1339
1360
|
const key = await importAesKeyDecryptOnly(keyBytes);
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1361
|
+
let pt;
|
|
1362
|
+
try {
|
|
1363
|
+
pt = await subtle.decrypt(
|
|
1364
|
+
{ name: "AES-GCM", iv },
|
|
1365
|
+
key,
|
|
1366
|
+
ciphertext
|
|
1367
|
+
);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
throw new HpkeError(
|
|
1370
|
+
"ERR_DECRYPT_FAILED",
|
|
1371
|
+
"failed to decrypt the message: tampered ciphertext",
|
|
1372
|
+
{ cause: err }
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1345
1375
|
return new Uint8Array(pt);
|
|
1346
1376
|
}
|
|
1347
1377
|
__name(decryptBytes, "decryptBytes");
|
|
@@ -1351,7 +1381,16 @@ async function decryptToString(keypair, message, opts) {
|
|
|
1351
1381
|
}
|
|
1352
1382
|
__name(decryptToString, "decryptToString");
|
|
1353
1383
|
async function decryptFromString(keypair, message, opts) {
|
|
1354
|
-
|
|
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
|
+
}
|
|
1355
1394
|
const plaintext = await decryptBytes(
|
|
1356
1395
|
keypair,
|
|
1357
1396
|
bytes,
|
|
@@ -1362,7 +1401,8 @@ async function decryptFromString(keypair, message, opts) {
|
|
|
1362
1401
|
__name(decryptFromString, "decryptFromString");
|
|
1363
1402
|
function validateKeysize(keysize) {
|
|
1364
1403
|
if (keysize !== 128 && keysize !== 256) {
|
|
1365
|
-
throw new
|
|
1404
|
+
throw new HpkeError(
|
|
1405
|
+
"ERR_INVALID_KEYSIZE",
|
|
1366
1406
|
`invalid keysize: ${keysize} (expected 128 or 256)`
|
|
1367
1407
|
);
|
|
1368
1408
|
}
|
|
@@ -1370,7 +1410,8 @@ function validateKeysize(keysize) {
|
|
|
1370
1410
|
__name(validateKeysize, "validateKeysize");
|
|
1371
1411
|
function validateRawKeyBytes(raw) {
|
|
1372
1412
|
if (raw.length !== 16 && raw.length !== 32) {
|
|
1373
|
-
throw new
|
|
1413
|
+
throw new HpkeError(
|
|
1414
|
+
"ERR_INVALID_AES_KEY",
|
|
1374
1415
|
`invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
|
|
1375
1416
|
);
|
|
1376
1417
|
}
|
|
@@ -1384,19 +1425,20 @@ function normalizeInfo(info) {
|
|
|
1384
1425
|
__name(normalizeInfo, "normalizeInfo");
|
|
1385
1426
|
async function exportAesKeyBytes(key) {
|
|
1386
1427
|
if (!key.extractable) {
|
|
1387
|
-
throw new
|
|
1428
|
+
throw new HpkeError(
|
|
1429
|
+
"ERR_INVALID_AES_KEY",
|
|
1388
1430
|
"aesKey must be extractable: its raw bytes are what get sealed"
|
|
1389
1431
|
);
|
|
1390
1432
|
}
|
|
1391
1433
|
return new Uint8Array(await subtle.exportKey("raw", key));
|
|
1392
1434
|
}
|
|
1393
1435
|
__name(exportAesKeyBytes, "exportAesKeyBytes");
|
|
1394
|
-
async function importAesKey(raw) {
|
|
1436
|
+
async function importAesKey(raw, extractable) {
|
|
1395
1437
|
return subtle.importKey(
|
|
1396
1438
|
"raw",
|
|
1397
1439
|
raw,
|
|
1398
1440
|
{ name: "AES-GCM" },
|
|
1399
|
-
|
|
1441
|
+
extractable,
|
|
1400
1442
|
["encrypt", "decrypt"]
|
|
1401
1443
|
);
|
|
1402
1444
|
}
|
|
@@ -1451,7 +1493,10 @@ async function keySchedule(sharedSecret, info) {
|
|
|
1451
1493
|
__name(keySchedule, "keySchedule");
|
|
1452
1494
|
async function openRawBytes(keypair, wrapped, opts) {
|
|
1453
1495
|
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1454
|
-
throw new
|
|
1496
|
+
throw new HpkeError(
|
|
1497
|
+
"ERR_MALFORMED_ENVELOPE",
|
|
1498
|
+
"malformed envelope: too short"
|
|
1499
|
+
);
|
|
1455
1500
|
}
|
|
1456
1501
|
const info = normalizeInfo(opts?.info);
|
|
1457
1502
|
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
@@ -1463,7 +1508,7 @@ async function openRawBytes(keypair, wrapped, opts) {
|
|
|
1463
1508
|
__name(openRawBytes, "openRawBytes");
|
|
1464
1509
|
async function openBytes(keypair, wrapped, opts) {
|
|
1465
1510
|
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1466
|
-
return importAesKey(keyBytes);
|
|
1511
|
+
return importAesKey(keyBytes, opts?.extractable ?? false);
|
|
1467
1512
|
}
|
|
1468
1513
|
__name(openBytes, "openBytes");
|
|
1469
1514
|
async function encryptKey(recipient, aesKey, opts) {
|
|
@@ -1473,6 +1518,12 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1473
1518
|
validateRawKeyBytes(aesKey);
|
|
1474
1519
|
keyBytes = aesKey;
|
|
1475
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
|
+
}
|
|
1476
1527
|
keyBytes = await exportAesKeyBytes(aesKey);
|
|
1477
1528
|
validateRawKeyBytes(keyBytes);
|
|
1478
1529
|
} else {
|
|
@@ -1487,8 +1538,12 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1487
1538
|
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1488
1539
|
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1489
1540
|
const wrapped = concat2(enc, ciphertext);
|
|
1490
|
-
|
|
1491
|
-
|
|
1541
|
+
if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
|
|
1542
|
+
const aesGcmKey = await importAesKey(
|
|
1543
|
+
keyBytes,
|
|
1544
|
+
opts?.extractable ?? false
|
|
1545
|
+
);
|
|
1546
|
+
return { wrapped, key: aesGcmKey };
|
|
1492
1547
|
}
|
|
1493
1548
|
__name(encryptKey, "encryptKey");
|
|
1494
1549
|
|
|
@@ -1518,24 +1573,44 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1518
1573
|
false,
|
|
1519
1574
|
["decrypt"]
|
|
1520
1575
|
);
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1576
|
+
let pt;
|
|
1577
|
+
try {
|
|
1578
|
+
pt = await subtle2.decrypt(
|
|
1579
|
+
{ name: "AES-GCM", iv: nonce },
|
|
1580
|
+
k,
|
|
1581
|
+
ciphertext
|
|
1582
|
+
);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
throw new HpkeError(
|
|
1585
|
+
"ERR_DECRYPT_FAILED",
|
|
1586
|
+
"failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
|
|
1587
|
+
{ cause: err }
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1526
1590
|
return new Uint8Array(pt);
|
|
1527
1591
|
}
|
|
1528
1592
|
__name(aeadOpen, "aeadOpen");
|
|
1529
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
|
+
}
|
|
1530
1600
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1531
|
-
const {
|
|
1601
|
+
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1532
1602
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
1533
1603
|
const ct = new Uint8Array(await subtle2.encrypt(
|
|
1534
1604
|
{ name: "AES-GCM", iv },
|
|
1535
1605
|
key,
|
|
1536
1606
|
plaintext
|
|
1537
1607
|
));
|
|
1538
|
-
return concat2(
|
|
1608
|
+
return concat2(
|
|
1609
|
+
i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
|
|
1610
|
+
wrapped,
|
|
1611
|
+
iv,
|
|
1612
|
+
ct
|
|
1613
|
+
);
|
|
1539
1614
|
}
|
|
1540
1615
|
__name(encryptBytes, "encryptBytes");
|
|
1541
1616
|
async function encryptToString(recipient, message, aesKey, opts) {
|
|
@@ -1601,7 +1676,8 @@ async function exportRawPublic(key) {
|
|
|
1601
1676
|
__name(exportRawPublic, "exportRawPublic");
|
|
1602
1677
|
async function importRawPublic(raw) {
|
|
1603
1678
|
if (raw.length !== ENC_LENGTH) {
|
|
1604
|
-
throw new
|
|
1679
|
+
throw new HpkeError(
|
|
1680
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1605
1681
|
`invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
|
|
1606
1682
|
);
|
|
1607
1683
|
}
|
|
@@ -1619,44 +1695,83 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1619
1695
|
return importRawPublic(recipient);
|
|
1620
1696
|
}
|
|
1621
1697
|
if (recipient instanceof CryptoKey) {
|
|
1622
|
-
|
|
1623
|
-
throw new Error("recipient CryptoKey must be a public key");
|
|
1624
|
-
}
|
|
1625
|
-
assertX25519Algorithm(recipient, "recipient CryptoKey");
|
|
1626
|
-
return recipient;
|
|
1698
|
+
return assertRecipientPublicKey(recipient, "recipient CryptoKey");
|
|
1627
1699
|
}
|
|
1628
1700
|
if (typeof recipient.publicKey === "string") {
|
|
1629
1701
|
const { publicKey, encoding } = recipient;
|
|
1630
|
-
|
|
1702
|
+
let decoded;
|
|
1703
|
+
try {
|
|
1704
|
+
decoded = fromString2(publicKey, encoding ?? "base64url");
|
|
1705
|
+
} catch (err) {
|
|
1706
|
+
throw new HpkeError(
|
|
1707
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1708
|
+
`recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
|
|
1709
|
+
{ cause: err }
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
return importRawPublic(decoded);
|
|
1631
1713
|
}
|
|
1632
1714
|
const pair = recipient;
|
|
1633
1715
|
if (pair.publicKey instanceof CryptoKey) {
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
return pair.publicKey;
|
|
1716
|
+
return assertRecipientPublicKey(
|
|
1717
|
+
pair.publicKey,
|
|
1718
|
+
"recipient keypair publicKey"
|
|
1719
|
+
);
|
|
1639
1720
|
}
|
|
1640
|
-
throw new
|
|
1721
|
+
throw new HpkeError(
|
|
1722
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1723
|
+
"unrecognized recipient key form"
|
|
1724
|
+
);
|
|
1641
1725
|
}
|
|
1642
1726
|
__name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
|
|
1643
|
-
function
|
|
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");
|
|
1744
|
+
function assertX25519Algorithm(key, context, code2) {
|
|
1644
1745
|
if (key.algorithm.name !== "X25519") {
|
|
1645
|
-
throw new
|
|
1746
|
+
throw new HpkeError(
|
|
1747
|
+
code2,
|
|
1646
1748
|
`${context} must be an X25519 key (got ${key.algorithm.name})`
|
|
1647
1749
|
);
|
|
1648
1750
|
}
|
|
1649
1751
|
}
|
|
1650
1752
|
__name(assertX25519Algorithm, "assertX25519Algorithm");
|
|
1651
1753
|
async function dh(priv, pub) {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
+
}
|
|
1657
1771
|
const bytes = new Uint8Array(bits);
|
|
1658
1772
|
if (bytes.every((b) => b === 0)) {
|
|
1659
|
-
throw new
|
|
1773
|
+
throw new HpkeError(
|
|
1774
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1660
1775
|
"X25519 shared secret is all-zero (small-order public key?)"
|
|
1661
1776
|
);
|
|
1662
1777
|
}
|
|
@@ -1695,9 +1810,33 @@ async function encap(pkR) {
|
|
|
1695
1810
|
__name(encap, "encap");
|
|
1696
1811
|
async function decap(enc, keypair) {
|
|
1697
1812
|
if (keypair.privateKey.type !== "private") {
|
|
1698
|
-
throw new
|
|
1813
|
+
throw new HpkeError(
|
|
1814
|
+
"ERR_INVALID_KEYPAIR",
|
|
1815
|
+
"keypair.privateKey must be a private key"
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
assertX25519Algorithm(
|
|
1819
|
+
keypair.privateKey,
|
|
1820
|
+
"keypair.privateKey",
|
|
1821
|
+
"ERR_INVALID_KEYPAIR"
|
|
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
|
+
);
|
|
1699
1834
|
}
|
|
1700
|
-
assertX25519Algorithm(
|
|
1835
|
+
assertX25519Algorithm(
|
|
1836
|
+
keypair.publicKey,
|
|
1837
|
+
"keypair.publicKey",
|
|
1838
|
+
"ERR_INVALID_KEYPAIR"
|
|
1839
|
+
);
|
|
1701
1840
|
const pkE = await subtle2.importKey(
|
|
1702
1841
|
"raw",
|
|
1703
1842
|
enc,
|
|
@@ -1711,6 +1850,45 @@ async function decap(enc, keypair) {
|
|
|
1711
1850
|
return extractAndExpand(dhBytes, kemContext);
|
|
1712
1851
|
}
|
|
1713
1852
|
__name(decap, "decap");
|
|
1853
|
+
async function keyId(key) {
|
|
1854
|
+
if (key.algorithm.name !== "AES-GCM") {
|
|
1855
|
+
throw new HpkeError(
|
|
1856
|
+
"ERR_INVALID_AES_KEY",
|
|
1857
|
+
`keyId needs an AES-GCM key (got ${key.algorithm.name})`
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
if (!key.usages.includes("encrypt")) {
|
|
1861
|
+
throw new HpkeError(
|
|
1862
|
+
"ERR_INVALID_AES_KEY",
|
|
1863
|
+
"keyId needs a key with the 'encrypt' usage"
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
const labelHash = new Uint8Array(
|
|
1867
|
+
await subtle2.digest("SHA-256", KEY_ID_LABEL)
|
|
1868
|
+
);
|
|
1869
|
+
const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
|
|
1870
|
+
const ciphertext = new Uint8Array(await subtle2.encrypt(
|
|
1871
|
+
{ name: "AES-GCM", iv: nonce },
|
|
1872
|
+
key,
|
|
1873
|
+
KEY_ID_LABEL
|
|
1874
|
+
));
|
|
1875
|
+
const digest2 = await subtle2.digest(
|
|
1876
|
+
"SHA-256",
|
|
1877
|
+
concat2(KEY_ID_LABEL, ciphertext)
|
|
1878
|
+
);
|
|
1879
|
+
return toString2(new Uint8Array(digest2), "base64url");
|
|
1880
|
+
}
|
|
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");
|
|
1714
1892
|
function i2osp(n, len) {
|
|
1715
1893
|
const out = new Uint8Array(len);
|
|
1716
1894
|
let v = n;
|
|
@@ -1743,8 +1921,10 @@ export {
|
|
|
1743
1921
|
encryptToString,
|
|
1744
1922
|
hmac,
|
|
1745
1923
|
i2osp,
|
|
1924
|
+
keyId,
|
|
1746
1925
|
labeledExpand,
|
|
1747
1926
|
labeledExtract,
|
|
1748
|
-
resolveRecipientPublicKey
|
|
1927
|
+
resolveRecipientPublicKey,
|
|
1928
|
+
verifyKeyId
|
|
1749
1929
|
};
|
|
1750
1930
|
//# sourceMappingURL=util.js.map
|