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/dist/util.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type RecipientKey = CryptoKey | CryptoKeyPair | Uint8Array | {
|
|
|
18
18
|
export declare function aeadSeal(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
19
19
|
export declare function aeadOpen(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Wrap a fresh (or supplied) AES key to `recipient`, then AES-GCM encrypt a
|
|
22
22
|
* message under it. The wrapped key, IV, and ciphertext are concatenated
|
|
23
23
|
* into a single self-describing envelope.
|
|
24
24
|
*
|
|
@@ -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,19 +1318,11 @@ 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;
|
|
1311
|
-
async function create2(recipient, opts) {
|
|
1312
|
-
const { wrapped, key } = await encryptKey(recipient, null, opts);
|
|
1313
|
-
return { enc: wrapped, key };
|
|
1314
|
-
}
|
|
1315
|
-
__name(create2, "create");
|
|
1316
|
-
async function seal(recipient, aesKey, opts) {
|
|
1317
|
-
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1318
|
-
return { enc: wrapped, key };
|
|
1319
|
-
}
|
|
1320
|
-
__name(seal, "seal");
|
|
1321
1326
|
var open = Object.assign(openBytes, {
|
|
1322
1327
|
raw: openRawBytes
|
|
1323
1328
|
});
|
|
@@ -1330,13 +1335,19 @@ var decrypt = Object.assign(decryptBytes, {
|
|
|
1330
1335
|
});
|
|
1331
1336
|
async function decryptBytes(keypair, message, opts) {
|
|
1332
1337
|
if (message.byteLength < WRAPPED_LEN_PREFIX) {
|
|
1333
|
-
throw new
|
|
1338
|
+
throw new HpkeError(
|
|
1339
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1340
|
+
"malformed message: too short"
|
|
1341
|
+
);
|
|
1334
1342
|
}
|
|
1335
1343
|
const wrappedLen = message[0] << 8 | message[1];
|
|
1336
1344
|
const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
|
|
1337
1345
|
const ctStart = ivStart + NN;
|
|
1338
1346
|
if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
|
|
1339
|
-
throw new
|
|
1347
|
+
throw new HpkeError(
|
|
1348
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1349
|
+
"malformed message: too short"
|
|
1350
|
+
);
|
|
1340
1351
|
}
|
|
1341
1352
|
const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
|
|
1342
1353
|
const iv = message.slice(ivStart, ctStart);
|
|
@@ -1347,11 +1358,20 @@ async function decryptBytes(keypair, message, opts) {
|
|
|
1347
1358
|
opts?.info !== void 0 ? { info: opts.info } : void 0
|
|
1348
1359
|
);
|
|
1349
1360
|
const key = await importAesKeyDecryptOnly(keyBytes);
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
+
}
|
|
1355
1375
|
return new Uint8Array(pt);
|
|
1356
1376
|
}
|
|
1357
1377
|
__name(decryptBytes, "decryptBytes");
|
|
@@ -1372,7 +1392,8 @@ async function decryptFromString(keypair, message, opts) {
|
|
|
1372
1392
|
__name(decryptFromString, "decryptFromString");
|
|
1373
1393
|
function validateKeysize(keysize) {
|
|
1374
1394
|
if (keysize !== 128 && keysize !== 256) {
|
|
1375
|
-
throw new
|
|
1395
|
+
throw new HpkeError(
|
|
1396
|
+
"ERR_INVALID_KEYSIZE",
|
|
1376
1397
|
`invalid keysize: ${keysize} (expected 128 or 256)`
|
|
1377
1398
|
);
|
|
1378
1399
|
}
|
|
@@ -1380,7 +1401,8 @@ function validateKeysize(keysize) {
|
|
|
1380
1401
|
__name(validateKeysize, "validateKeysize");
|
|
1381
1402
|
function validateRawKeyBytes(raw) {
|
|
1382
1403
|
if (raw.length !== 16 && raw.length !== 32) {
|
|
1383
|
-
throw new
|
|
1404
|
+
throw new HpkeError(
|
|
1405
|
+
"ERR_INVALID_AES_KEY",
|
|
1384
1406
|
`invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
|
|
1385
1407
|
);
|
|
1386
1408
|
}
|
|
@@ -1394,19 +1416,20 @@ function normalizeInfo(info) {
|
|
|
1394
1416
|
__name(normalizeInfo, "normalizeInfo");
|
|
1395
1417
|
async function exportAesKeyBytes(key) {
|
|
1396
1418
|
if (!key.extractable) {
|
|
1397
|
-
throw new
|
|
1419
|
+
throw new HpkeError(
|
|
1420
|
+
"ERR_INVALID_AES_KEY",
|
|
1398
1421
|
"aesKey must be extractable: its raw bytes are what get sealed"
|
|
1399
1422
|
);
|
|
1400
1423
|
}
|
|
1401
1424
|
return new Uint8Array(await subtle.exportKey("raw", key));
|
|
1402
1425
|
}
|
|
1403
1426
|
__name(exportAesKeyBytes, "exportAesKeyBytes");
|
|
1404
|
-
async function importAesKey(raw) {
|
|
1427
|
+
async function importAesKey(raw, extractable) {
|
|
1405
1428
|
return subtle.importKey(
|
|
1406
1429
|
"raw",
|
|
1407
1430
|
raw,
|
|
1408
1431
|
{ name: "AES-GCM" },
|
|
1409
|
-
|
|
1432
|
+
extractable,
|
|
1410
1433
|
["encrypt", "decrypt"]
|
|
1411
1434
|
);
|
|
1412
1435
|
}
|
|
@@ -1461,7 +1484,10 @@ async function keySchedule(sharedSecret, info) {
|
|
|
1461
1484
|
__name(keySchedule, "keySchedule");
|
|
1462
1485
|
async function openRawBytes(keypair, wrapped, opts) {
|
|
1463
1486
|
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1464
|
-
throw new
|
|
1487
|
+
throw new HpkeError(
|
|
1488
|
+
"ERR_MALFORMED_ENVELOPE",
|
|
1489
|
+
"malformed envelope: too short"
|
|
1490
|
+
);
|
|
1465
1491
|
}
|
|
1466
1492
|
const info = normalizeInfo(opts?.info);
|
|
1467
1493
|
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
@@ -1473,7 +1499,7 @@ async function openRawBytes(keypair, wrapped, opts) {
|
|
|
1473
1499
|
__name(openRawBytes, "openRawBytes");
|
|
1474
1500
|
async function openBytes(keypair, wrapped, opts) {
|
|
1475
1501
|
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1476
|
-
return importAesKey(keyBytes);
|
|
1502
|
+
return importAesKey(keyBytes, opts?.extractable ?? false);
|
|
1477
1503
|
}
|
|
1478
1504
|
__name(openBytes, "openBytes");
|
|
1479
1505
|
async function encryptKey(recipient, aesKey, opts) {
|
|
@@ -1497,7 +1523,10 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1497
1523
|
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1498
1524
|
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1499
1525
|
const wrapped = concat2(enc, ciphertext);
|
|
1500
|
-
const aesGcmKey = await importAesKey(
|
|
1526
|
+
const aesGcmKey = await importAesKey(
|
|
1527
|
+
keyBytes,
|
|
1528
|
+
opts?.extractable ?? false
|
|
1529
|
+
);
|
|
1501
1530
|
return { wrapped, key: aesGcmKey };
|
|
1502
1531
|
}
|
|
1503
1532
|
__name(encryptKey, "encryptKey");
|
|
@@ -1528,34 +1557,38 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1528
1557
|
false,
|
|
1529
1558
|
["decrypt"]
|
|
1530
1559
|
);
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
+
}
|
|
1536
1574
|
return new Uint8Array(pt);
|
|
1537
1575
|
}
|
|
1538
1576
|
__name(aeadOpen, "aeadOpen");
|
|
1539
1577
|
async function encryptBytes(recipient, message, aesKey, opts) {
|
|
1540
1578
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1541
|
-
|
|
1542
|
-
let key;
|
|
1543
|
-
if (!aesKey) {
|
|
1544
|
-
const keys = await create2(recipient, opts);
|
|
1545
|
-
enc = keys.enc;
|
|
1546
|
-
key = keys.key;
|
|
1547
|
-
} else {
|
|
1548
|
-
const keys = await seal(recipient, aesKey, opts);
|
|
1549
|
-
enc = keys.enc;
|
|
1550
|
-
key = keys.key;
|
|
1551
|
-
}
|
|
1579
|
+
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1552
1580
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
1553
1581
|
const ct = new Uint8Array(await subtle2.encrypt(
|
|
1554
1582
|
{ name: "AES-GCM", iv },
|
|
1555
1583
|
key,
|
|
1556
1584
|
plaintext
|
|
1557
1585
|
));
|
|
1558
|
-
return concat2(
|
|
1586
|
+
return concat2(
|
|
1587
|
+
i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
|
|
1588
|
+
wrapped,
|
|
1589
|
+
iv,
|
|
1590
|
+
ct
|
|
1591
|
+
);
|
|
1559
1592
|
}
|
|
1560
1593
|
__name(encryptBytes, "encryptBytes");
|
|
1561
1594
|
async function encryptToString(recipient, message, aesKey, opts) {
|
|
@@ -1621,7 +1654,8 @@ async function exportRawPublic(key) {
|
|
|
1621
1654
|
__name(exportRawPublic, "exportRawPublic");
|
|
1622
1655
|
async function importRawPublic(raw) {
|
|
1623
1656
|
if (raw.length !== ENC_LENGTH) {
|
|
1624
|
-
throw new
|
|
1657
|
+
throw new HpkeError(
|
|
1658
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1625
1659
|
`invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
|
|
1626
1660
|
);
|
|
1627
1661
|
}
|
|
@@ -1640,9 +1674,16 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1640
1674
|
}
|
|
1641
1675
|
if (recipient instanceof CryptoKey) {
|
|
1642
1676
|
if (recipient.type !== "public") {
|
|
1643
|
-
throw new
|
|
1677
|
+
throw new HpkeError(
|
|
1678
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1679
|
+
"recipient CryptoKey must be a public key"
|
|
1680
|
+
);
|
|
1644
1681
|
}
|
|
1645
|
-
assertX25519Algorithm(
|
|
1682
|
+
assertX25519Algorithm(
|
|
1683
|
+
recipient,
|
|
1684
|
+
"recipient CryptoKey",
|
|
1685
|
+
"ERR_INVALID_RECIPIENT_KEY"
|
|
1686
|
+
);
|
|
1646
1687
|
return recipient;
|
|
1647
1688
|
}
|
|
1648
1689
|
if (typeof recipient.publicKey === "string") {
|
|
@@ -1652,17 +1693,28 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1652
1693
|
const pair = recipient;
|
|
1653
1694
|
if (pair.publicKey instanceof CryptoKey) {
|
|
1654
1695
|
if (pair.publicKey.type !== "public") {
|
|
1655
|
-
throw new
|
|
1696
|
+
throw new HpkeError(
|
|
1697
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1698
|
+
"recipient keypair publicKey must be a public key"
|
|
1699
|
+
);
|
|
1656
1700
|
}
|
|
1657
|
-
assertX25519Algorithm(
|
|
1701
|
+
assertX25519Algorithm(
|
|
1702
|
+
pair.publicKey,
|
|
1703
|
+
"recipient keypair publicKey",
|
|
1704
|
+
"ERR_INVALID_RECIPIENT_KEY"
|
|
1705
|
+
);
|
|
1658
1706
|
return pair.publicKey;
|
|
1659
1707
|
}
|
|
1660
|
-
throw new
|
|
1708
|
+
throw new HpkeError(
|
|
1709
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1710
|
+
"unrecognized recipient key form"
|
|
1711
|
+
);
|
|
1661
1712
|
}
|
|
1662
1713
|
__name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
|
|
1663
|
-
function assertX25519Algorithm(key, context) {
|
|
1714
|
+
function assertX25519Algorithm(key, context, code2) {
|
|
1664
1715
|
if (key.algorithm.name !== "X25519") {
|
|
1665
|
-
throw new
|
|
1716
|
+
throw new HpkeError(
|
|
1717
|
+
code2,
|
|
1666
1718
|
`${context} must be an X25519 key (got ${key.algorithm.name})`
|
|
1667
1719
|
);
|
|
1668
1720
|
}
|
|
@@ -1676,7 +1728,8 @@ async function dh(priv, pub) {
|
|
|
1676
1728
|
);
|
|
1677
1729
|
const bytes = new Uint8Array(bits);
|
|
1678
1730
|
if (bytes.every((b) => b === 0)) {
|
|
1679
|
-
throw new
|
|
1731
|
+
throw new HpkeError(
|
|
1732
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1680
1733
|
"X25519 shared secret is all-zero (small-order public key?)"
|
|
1681
1734
|
);
|
|
1682
1735
|
}
|
|
@@ -1715,9 +1768,16 @@ async function encap(pkR) {
|
|
|
1715
1768
|
__name(encap, "encap");
|
|
1716
1769
|
async function decap(enc, keypair) {
|
|
1717
1770
|
if (keypair.privateKey.type !== "private") {
|
|
1718
|
-
throw new
|
|
1771
|
+
throw new HpkeError(
|
|
1772
|
+
"ERR_INVALID_KEYPAIR",
|
|
1773
|
+
"keypair.privateKey must be a private key"
|
|
1774
|
+
);
|
|
1719
1775
|
}
|
|
1720
|
-
assertX25519Algorithm(
|
|
1776
|
+
assertX25519Algorithm(
|
|
1777
|
+
keypair.privateKey,
|
|
1778
|
+
"keypair.privateKey",
|
|
1779
|
+
"ERR_INVALID_KEYPAIR"
|
|
1780
|
+
);
|
|
1721
1781
|
const pkE = await subtle2.importKey(
|
|
1722
1782
|
"raw",
|
|
1723
1783
|
enc,
|
|
@@ -1731,6 +1791,35 @@ async function decap(enc, keypair) {
|
|
|
1731
1791
|
return extractAndExpand(dhBytes, kemContext);
|
|
1732
1792
|
}
|
|
1733
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");
|
|
1734
1823
|
function i2osp(n, len) {
|
|
1735
1824
|
const out = new Uint8Array(len);
|
|
1736
1825
|
let v = n;
|
|
@@ -1763,6 +1852,7 @@ export {
|
|
|
1763
1852
|
encryptToString,
|
|
1764
1853
|
hmac,
|
|
1765
1854
|
i2osp,
|
|
1855
|
+
keyId,
|
|
1766
1856
|
labeledExpand,
|
|
1767
1857
|
labeledExtract,
|
|
1768
1858
|
resolveRecipientPublicKey
|