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/dist/util.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export declare function encryptBytes(recipient: RecipientKey, message: Uint8Arra
|
|
|
49
49
|
info?: Uint8Array | string;
|
|
50
50
|
}): Promise<Uint8Array>;
|
|
51
51
|
/**
|
|
52
|
-
* Like `encrypt`, but encodes the envelope bytes to a string
|
|
52
|
+
* Like `encrypt`, but encodes the envelope bytes to a string -- handy for
|
|
53
53
|
* transports that carry text (JSON, URLs, headers). Decode with
|
|
54
54
|
* `fromString(...)` (or any matching decoder) and pass the bytes to
|
|
55
55
|
* `decrypt` / `decrypt.asString`. Exposed as `encrypt.asString`.
|
|
@@ -85,6 +85,37 @@ export declare function encap(pkR: CryptoKey): Promise<{
|
|
|
85
85
|
enc: Uint8Array;
|
|
86
86
|
}>;
|
|
87
87
|
export declare function decap(enc: Uint8Array, keypair: CryptoKeyPair): Promise<Uint8Array>;
|
|
88
|
+
/**
|
|
89
|
+
* A stable fingerprint for an AES-GCM key: the same key bytes always give
|
|
90
|
+
* the same string, and two different keys effectively never collide.
|
|
91
|
+
*
|
|
92
|
+
* Derived from the key's *behavior* rather than its bytes, so it works on
|
|
93
|
+
* a non-extractable key -- including one rehydrated from IndexedDB, and
|
|
94
|
+
* the keys `create` / `encryptKey` / `open` return by default. The obvious
|
|
95
|
+
* implementation, `exportKey('raw')` then SHA-256, would force every such
|
|
96
|
+
* key to be extractable. HKDF is not an option either: WebCrypto cannot
|
|
97
|
+
* convert an AES-GCM key into an HKDF key, and `deriveBits` only accepts a
|
|
98
|
+
* key imported under HKDF, which needs the raw bytes a non-extractable key
|
|
99
|
+
* withholds. AES-GCM as a PRF is the only primitive such a key still
|
|
100
|
+
* exposes.
|
|
101
|
+
*
|
|
102
|
+
* The derivation is fixed forever (see `src/constants.ts`):
|
|
103
|
+
*
|
|
104
|
+
* ```
|
|
105
|
+
* LABEL = utf8("simple-hpke/keyId/v1")
|
|
106
|
+
* N = SHA-256(LABEL)[0..12] // fixed nonce
|
|
107
|
+
* C = AES-GCM(key, N, LABEL) // ciphertext || 16-byte tag
|
|
108
|
+
* keyId = base64url(SHA-256(LABEL || C))
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @param key An AES-GCM `CryptoKey` with the `encrypt` usage. It does not
|
|
112
|
+
* need to be extractable.
|
|
113
|
+
* @returns The fingerprint: the full 32-byte digest, base64url, unpadded
|
|
114
|
+
* (43 characters).
|
|
115
|
+
* @throws {HpkeError} `ERR_INVALID_AES_KEY` if `key` is not an AES-GCM key
|
|
116
|
+
* or cannot encrypt.
|
|
117
|
+
*/
|
|
118
|
+
export declare function keyId(key: CryptoKey): Promise<string>;
|
|
88
119
|
export declare function i2osp(n: number, len: number): Uint8Array;
|
|
89
120
|
export declare function concat(...arrays: Uint8Array[]): Uint8Array;
|
|
90
121
|
//# 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":"AAAA,OAAO,EAGH,KAAK,kBAAkB,EAC1B,MAAM,aAAa,CAAA;AAepB;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAClB,SAAS,GACT,aAAa,GACb,UAAU,GACV;IAAE,SAAS,EAAC,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAAE,CAAA;AAExD;;GAEG;AACH,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,SAAS,EAAC,UAAU,GACtB,OAAO,CAAC,UAAU,CAAC,CAcpB;AAED,wBAAsB,QAAQ,CAC1B,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,UAAU,EAChB,UAAU,EAAC,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC,CA2BpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,YAAY,CAC9B,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAC1B,GACH,OAAO,CAAC,UAAU,CAAC,CAoBpB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,eAAe,CACjC,SAAS,EAAC,YAAY,EACtB,OAAO,EAAC,UAAU,GAAC,MAAM,EACzB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IACF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IACb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAC,kBAAkB,CAAA;CAC/B,GACH,OAAO,CAAC,MAAM,CAAC,CAGhB;AAQD,wBAAsB,IAAI,CAAE,GAAG,EAAC,UAAU,EAAE,IAAI,EAAC,UAAU,GAAE,OAAO,CAAC,UAAU,CAAC,CAS/E;AAiCD,wBAAsB,cAAc,CAChC,OAAO,EAAC,UAAU,EAClB,IAAI,EAAC,UAAU,EACf,KAAK,EAAC,MAAM,EACZ,GAAG,EAAC,UAAU,GAChB,OAAO,CAAC,UAAU,CAAC,CAQpB;AAGD,wBAAsB,aAAa,CAC/B,OAAO,EAAC,UAAU,EAClB,GAAG,EAAC,UAAU,EACd,KAAK,EAAC,MAAM,EACZ,IAAI,EAAC,UAAU,EACf,MAAM,EAAC,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CASpB;AA+BD,wBAAsB,yBAAyB,CAC3C,SAAS,EAAC,YAAY,GACxB,OAAO,CAAC,SAAS,CAAC,CAkDnB;AAmED,wBAAsB,KAAK,CACvB,GAAG,EAAC,SAAS,GACf,OAAO,CAAC;IAAE,YAAY,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,UAAU,CAAA;CAAE,CAAC,CAarD;AAID,wBAAsB,KAAK,CACvB,GAAG,EAAC,UAAU,EACd,OAAO,EAAC,aAAa,GACvB,OAAO,CAAC,UAAU,CAAC,CAwBpB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,KAAK,CAAE,GAAG,EAAC,SAAS,GAAE,OAAO,CAAC,MAAM,CAAC,CAuC1D;AAKD,wBAAgB,KAAK,CAAE,CAAC,EAAC,MAAM,EAAE,GAAG,EAAC,MAAM,GAAE,UAAU,CAQtD;AAED,wBAAgB,MAAM,CAAE,GAAG,MAAM,EAAC,UAAU,EAAE,GAAE,UAAU,CAUzD"}
|
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");
|
|
@@ -1362,7 +1392,8 @@ async function decryptFromString(keypair, message, opts) {
|
|
|
1362
1392
|
__name(decryptFromString, "decryptFromString");
|
|
1363
1393
|
function validateKeysize(keysize) {
|
|
1364
1394
|
if (keysize !== 128 && keysize !== 256) {
|
|
1365
|
-
throw new
|
|
1395
|
+
throw new HpkeError(
|
|
1396
|
+
"ERR_INVALID_KEYSIZE",
|
|
1366
1397
|
`invalid keysize: ${keysize} (expected 128 or 256)`
|
|
1367
1398
|
);
|
|
1368
1399
|
}
|
|
@@ -1370,7 +1401,8 @@ function validateKeysize(keysize) {
|
|
|
1370
1401
|
__name(validateKeysize, "validateKeysize");
|
|
1371
1402
|
function validateRawKeyBytes(raw) {
|
|
1372
1403
|
if (raw.length !== 16 && raw.length !== 32) {
|
|
1373
|
-
throw new
|
|
1404
|
+
throw new HpkeError(
|
|
1405
|
+
"ERR_INVALID_AES_KEY",
|
|
1374
1406
|
`invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
|
|
1375
1407
|
);
|
|
1376
1408
|
}
|
|
@@ -1384,19 +1416,20 @@ function normalizeInfo(info) {
|
|
|
1384
1416
|
__name(normalizeInfo, "normalizeInfo");
|
|
1385
1417
|
async function exportAesKeyBytes(key) {
|
|
1386
1418
|
if (!key.extractable) {
|
|
1387
|
-
throw new
|
|
1419
|
+
throw new HpkeError(
|
|
1420
|
+
"ERR_INVALID_AES_KEY",
|
|
1388
1421
|
"aesKey must be extractable: its raw bytes are what get sealed"
|
|
1389
1422
|
);
|
|
1390
1423
|
}
|
|
1391
1424
|
return new Uint8Array(await subtle.exportKey("raw", key));
|
|
1392
1425
|
}
|
|
1393
1426
|
__name(exportAesKeyBytes, "exportAesKeyBytes");
|
|
1394
|
-
async function importAesKey(raw) {
|
|
1427
|
+
async function importAesKey(raw, extractable) {
|
|
1395
1428
|
return subtle.importKey(
|
|
1396
1429
|
"raw",
|
|
1397
1430
|
raw,
|
|
1398
1431
|
{ name: "AES-GCM" },
|
|
1399
|
-
|
|
1432
|
+
extractable,
|
|
1400
1433
|
["encrypt", "decrypt"]
|
|
1401
1434
|
);
|
|
1402
1435
|
}
|
|
@@ -1451,7 +1484,10 @@ async function keySchedule(sharedSecret, info) {
|
|
|
1451
1484
|
__name(keySchedule, "keySchedule");
|
|
1452
1485
|
async function openRawBytes(keypair, wrapped, opts) {
|
|
1453
1486
|
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1454
|
-
throw new
|
|
1487
|
+
throw new HpkeError(
|
|
1488
|
+
"ERR_MALFORMED_ENVELOPE",
|
|
1489
|
+
"malformed envelope: too short"
|
|
1490
|
+
);
|
|
1455
1491
|
}
|
|
1456
1492
|
const info = normalizeInfo(opts?.info);
|
|
1457
1493
|
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
@@ -1463,7 +1499,7 @@ async function openRawBytes(keypair, wrapped, opts) {
|
|
|
1463
1499
|
__name(openRawBytes, "openRawBytes");
|
|
1464
1500
|
async function openBytes(keypair, wrapped, opts) {
|
|
1465
1501
|
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1466
|
-
return importAesKey(keyBytes);
|
|
1502
|
+
return importAesKey(keyBytes, opts?.extractable ?? false);
|
|
1467
1503
|
}
|
|
1468
1504
|
__name(openBytes, "openBytes");
|
|
1469
1505
|
async function encryptKey(recipient, aesKey, opts) {
|
|
@@ -1487,8 +1523,11 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1487
1523
|
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1488
1524
|
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1489
1525
|
const wrapped = concat2(enc, ciphertext);
|
|
1490
|
-
const aesGcmKey = await importAesKey(
|
|
1491
|
-
|
|
1526
|
+
const aesGcmKey = await importAesKey(
|
|
1527
|
+
keyBytes,
|
|
1528
|
+
opts?.extractable ?? false
|
|
1529
|
+
);
|
|
1530
|
+
return { wrapped, key: aesGcmKey };
|
|
1492
1531
|
}
|
|
1493
1532
|
__name(encryptKey, "encryptKey");
|
|
1494
1533
|
|
|
@@ -1518,24 +1557,38 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1518
1557
|
false,
|
|
1519
1558
|
["decrypt"]
|
|
1520
1559
|
);
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1560
|
+
let pt;
|
|
1561
|
+
try {
|
|
1562
|
+
pt = await subtle2.decrypt(
|
|
1563
|
+
{ name: "AES-GCM", iv: nonce },
|
|
1564
|
+
k,
|
|
1565
|
+
ciphertext
|
|
1566
|
+
);
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
throw new HpkeError(
|
|
1569
|
+
"ERR_DECRYPT_FAILED",
|
|
1570
|
+
"failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
|
|
1571
|
+
{ cause: err }
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1526
1574
|
return new Uint8Array(pt);
|
|
1527
1575
|
}
|
|
1528
1576
|
__name(aeadOpen, "aeadOpen");
|
|
1529
1577
|
async function encryptBytes(recipient, message, aesKey, opts) {
|
|
1530
1578
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1531
|
-
const {
|
|
1579
|
+
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1532
1580
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
1533
1581
|
const ct = new Uint8Array(await subtle2.encrypt(
|
|
1534
1582
|
{ name: "AES-GCM", iv },
|
|
1535
1583
|
key,
|
|
1536
1584
|
plaintext
|
|
1537
1585
|
));
|
|
1538
|
-
return concat2(
|
|
1586
|
+
return concat2(
|
|
1587
|
+
i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
|
|
1588
|
+
wrapped,
|
|
1589
|
+
iv,
|
|
1590
|
+
ct
|
|
1591
|
+
);
|
|
1539
1592
|
}
|
|
1540
1593
|
__name(encryptBytes, "encryptBytes");
|
|
1541
1594
|
async function encryptToString(recipient, message, aesKey, opts) {
|
|
@@ -1601,7 +1654,8 @@ async function exportRawPublic(key) {
|
|
|
1601
1654
|
__name(exportRawPublic, "exportRawPublic");
|
|
1602
1655
|
async function importRawPublic(raw) {
|
|
1603
1656
|
if (raw.length !== ENC_LENGTH) {
|
|
1604
|
-
throw new
|
|
1657
|
+
throw new HpkeError(
|
|
1658
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1605
1659
|
`invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
|
|
1606
1660
|
);
|
|
1607
1661
|
}
|
|
@@ -1620,9 +1674,16 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1620
1674
|
}
|
|
1621
1675
|
if (recipient instanceof CryptoKey) {
|
|
1622
1676
|
if (recipient.type !== "public") {
|
|
1623
|
-
throw new
|
|
1677
|
+
throw new HpkeError(
|
|
1678
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1679
|
+
"recipient CryptoKey must be a public key"
|
|
1680
|
+
);
|
|
1624
1681
|
}
|
|
1625
|
-
assertX25519Algorithm(
|
|
1682
|
+
assertX25519Algorithm(
|
|
1683
|
+
recipient,
|
|
1684
|
+
"recipient CryptoKey",
|
|
1685
|
+
"ERR_INVALID_RECIPIENT_KEY"
|
|
1686
|
+
);
|
|
1626
1687
|
return recipient;
|
|
1627
1688
|
}
|
|
1628
1689
|
if (typeof recipient.publicKey === "string") {
|
|
@@ -1632,17 +1693,28 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1632
1693
|
const pair = recipient;
|
|
1633
1694
|
if (pair.publicKey instanceof CryptoKey) {
|
|
1634
1695
|
if (pair.publicKey.type !== "public") {
|
|
1635
|
-
throw new
|
|
1696
|
+
throw new HpkeError(
|
|
1697
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1698
|
+
"recipient keypair publicKey must be a public key"
|
|
1699
|
+
);
|
|
1636
1700
|
}
|
|
1637
|
-
assertX25519Algorithm(
|
|
1701
|
+
assertX25519Algorithm(
|
|
1702
|
+
pair.publicKey,
|
|
1703
|
+
"recipient keypair publicKey",
|
|
1704
|
+
"ERR_INVALID_RECIPIENT_KEY"
|
|
1705
|
+
);
|
|
1638
1706
|
return pair.publicKey;
|
|
1639
1707
|
}
|
|
1640
|
-
throw new
|
|
1708
|
+
throw new HpkeError(
|
|
1709
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1710
|
+
"unrecognized recipient key form"
|
|
1711
|
+
);
|
|
1641
1712
|
}
|
|
1642
1713
|
__name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
|
|
1643
|
-
function assertX25519Algorithm(key, context) {
|
|
1714
|
+
function assertX25519Algorithm(key, context, code2) {
|
|
1644
1715
|
if (key.algorithm.name !== "X25519") {
|
|
1645
|
-
throw new
|
|
1716
|
+
throw new HpkeError(
|
|
1717
|
+
code2,
|
|
1646
1718
|
`${context} must be an X25519 key (got ${key.algorithm.name})`
|
|
1647
1719
|
);
|
|
1648
1720
|
}
|
|
@@ -1656,7 +1728,8 @@ async function dh(priv, pub) {
|
|
|
1656
1728
|
);
|
|
1657
1729
|
const bytes = new Uint8Array(bits);
|
|
1658
1730
|
if (bytes.every((b) => b === 0)) {
|
|
1659
|
-
throw new
|
|
1731
|
+
throw new HpkeError(
|
|
1732
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1660
1733
|
"X25519 shared secret is all-zero (small-order public key?)"
|
|
1661
1734
|
);
|
|
1662
1735
|
}
|
|
@@ -1695,9 +1768,16 @@ async function encap(pkR) {
|
|
|
1695
1768
|
__name(encap, "encap");
|
|
1696
1769
|
async function decap(enc, keypair) {
|
|
1697
1770
|
if (keypair.privateKey.type !== "private") {
|
|
1698
|
-
throw new
|
|
1771
|
+
throw new HpkeError(
|
|
1772
|
+
"ERR_INVALID_KEYPAIR",
|
|
1773
|
+
"keypair.privateKey must be a private key"
|
|
1774
|
+
);
|
|
1699
1775
|
}
|
|
1700
|
-
assertX25519Algorithm(
|
|
1776
|
+
assertX25519Algorithm(
|
|
1777
|
+
keypair.privateKey,
|
|
1778
|
+
"keypair.privateKey",
|
|
1779
|
+
"ERR_INVALID_KEYPAIR"
|
|
1780
|
+
);
|
|
1701
1781
|
const pkE = await subtle2.importKey(
|
|
1702
1782
|
"raw",
|
|
1703
1783
|
enc,
|
|
@@ -1711,6 +1791,35 @@ async function decap(enc, keypair) {
|
|
|
1711
1791
|
return extractAndExpand(dhBytes, kemContext);
|
|
1712
1792
|
}
|
|
1713
1793
|
__name(decap, "decap");
|
|
1794
|
+
async function keyId(key) {
|
|
1795
|
+
if (key.algorithm.name !== "AES-GCM") {
|
|
1796
|
+
throw new HpkeError(
|
|
1797
|
+
"ERR_INVALID_AES_KEY",
|
|
1798
|
+
`keyId needs an AES-GCM key (got ${key.algorithm.name})`
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
if (!key.usages.includes("encrypt")) {
|
|
1802
|
+
throw new HpkeError(
|
|
1803
|
+
"ERR_INVALID_AES_KEY",
|
|
1804
|
+
"keyId needs a key with the 'encrypt' usage"
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const labelHash = new Uint8Array(
|
|
1808
|
+
await subtle2.digest("SHA-256", KEY_ID_LABEL)
|
|
1809
|
+
);
|
|
1810
|
+
const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
|
|
1811
|
+
const ciphertext = new Uint8Array(await subtle2.encrypt(
|
|
1812
|
+
{ name: "AES-GCM", iv: nonce },
|
|
1813
|
+
key,
|
|
1814
|
+
KEY_ID_LABEL
|
|
1815
|
+
));
|
|
1816
|
+
const digest2 = await subtle2.digest(
|
|
1817
|
+
"SHA-256",
|
|
1818
|
+
concat2(KEY_ID_LABEL, ciphertext)
|
|
1819
|
+
);
|
|
1820
|
+
return toString2(new Uint8Array(digest2), "base64url");
|
|
1821
|
+
}
|
|
1822
|
+
__name(keyId, "keyId");
|
|
1714
1823
|
function i2osp(n, len) {
|
|
1715
1824
|
const out = new Uint8Array(len);
|
|
1716
1825
|
let v = n;
|
|
@@ -1743,6 +1852,7 @@ export {
|
|
|
1743
1852
|
encryptToString,
|
|
1744
1853
|
hmac,
|
|
1745
1854
|
i2osp,
|
|
1855
|
+
keyId,
|
|
1746
1856
|
labeledExpand,
|
|
1747
1857
|
labeledExtract,
|
|
1748
1858
|
resolveRecipientPublicKey
|