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/index.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { type RecipientKey, encryptToString, encryptBytes } from './util';
|
|
1
|
+
import { type RecipientKey, encryptToString, encryptBytes, keyId, verifyKeyId } from './util';
|
|
2
|
+
export { keyId, verifyKeyId };
|
|
3
|
+
export { HpkeError, type HpkeErrorCode } from './errors';
|
|
2
4
|
/**
|
|
3
5
|
* Create a new AES key for the given public key.
|
|
4
6
|
*
|
|
5
7
|
* @param recipient Public key for decryptor
|
|
6
|
-
* @param opts `size` and `
|
|
7
|
-
* @returns {{
|
|
8
|
+
* @param opts `size`, `info`, and `extractable`
|
|
9
|
+
* @returns {{ wrapped, key }} The wrapped envelope bytes and the generated
|
|
8
10
|
* AES-GCM key.
|
|
9
11
|
*/
|
|
10
12
|
export declare function create(recipient: RecipientKey, opts?: {
|
|
11
13
|
size?: 128 | 256;
|
|
12
14
|
info?: Uint8Array | string;
|
|
15
|
+
extractable?: boolean;
|
|
13
16
|
}): Promise<{
|
|
14
|
-
|
|
17
|
+
wrapped: Uint8Array<ArrayBufferLike>;
|
|
15
18
|
key: CryptoKey;
|
|
16
19
|
}>;
|
|
17
20
|
/**
|
|
@@ -46,7 +49,7 @@ export declare const decrypt: typeof decryptBytes & {
|
|
|
46
49
|
*
|
|
47
50
|
* @param keypair The same recipient `CryptoKeyPair` used to `encrypt`.
|
|
48
51
|
* @param message The envelope returned by `encrypt`.
|
|
49
|
-
* @param opts `info`
|
|
52
|
+
* @param opts `info` -- must match the value passed to `encrypt`.
|
|
50
53
|
* @returns The decrypted plaintext bytes. Use `decrypt.asString` for a string.
|
|
51
54
|
*/
|
|
52
55
|
declare function decryptBytes(keypair: CryptoKeyPair, message: Uint8Array, opts?: {
|
|
@@ -58,7 +61,7 @@ declare function decryptBytes(keypair: CryptoKeyPair, message: Uint8Array, opts?
|
|
|
58
61
|
*
|
|
59
62
|
* @param keypair The same recipient `CryptoKeyPair` used to `encrypt`.
|
|
60
63
|
* @param message The envelope returned by `encrypt`.
|
|
61
|
-
* @param opts `info`
|
|
64
|
+
* @param opts `info` -- must match the value passed to `encrypt`.
|
|
62
65
|
* @returns The decrypted plaintext as a string.
|
|
63
66
|
*/
|
|
64
67
|
declare function decryptToString(keypair: CryptoKeyPair, message: Uint8Array, opts?: {
|
|
@@ -91,24 +94,28 @@ declare function decryptFromString(keypair: CryptoKeyPair, message: string, opts
|
|
|
91
94
|
* importing it as an AES-GCM `CryptoKey`. Exposed as `open.raw`.
|
|
92
95
|
*
|
|
93
96
|
* @param keypair The same X25519 `CryptoKeyPair` used to create or encryptKey.
|
|
94
|
-
* @param wrapped The `
|
|
95
|
-
* @param opts `info`
|
|
97
|
+
* @param wrapped The `wrapped` bytes returned by `create` or `encryptKey`.
|
|
98
|
+
* @param opts `info` -- must match the value passed to `create` or
|
|
99
|
+
* `encryptKey`.
|
|
96
100
|
* @returns The recovered key bytes (16 or 32 bytes, matching whatever
|
|
97
101
|
* was wrapped).
|
|
98
102
|
*/
|
|
99
103
|
declare function openRawBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
|
|
100
|
-
info
|
|
104
|
+
info?: Uint8Array | string;
|
|
101
105
|
}): Promise<Uint8Array>;
|
|
102
106
|
/**
|
|
103
107
|
* Recover an AES key that was wrapped with `create` or `encryptKey`.
|
|
104
108
|
*
|
|
105
109
|
* @param keypair The same X25519 `CryptoKeyPair` used to create or encryptKey.
|
|
106
|
-
* @param wrapped The `
|
|
107
|
-
* @param opts `info`
|
|
108
|
-
*
|
|
110
|
+
* @param wrapped The `wrapped` bytes returned by `create` or `encryptKey`.
|
|
111
|
+
* @param opts `info` -- must match the value passed to `create` or
|
|
112
|
+
* `encryptKey` -- and `extractable` (default false).
|
|
113
|
+
* @returns The recovered AES-GCM `CryptoKey`, non-extractable unless
|
|
114
|
+
* `opts.extractable` is true.
|
|
109
115
|
*/
|
|
110
116
|
declare function openBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
|
|
111
|
-
info
|
|
117
|
+
info?: Uint8Array | string;
|
|
118
|
+
extractable?: boolean;
|
|
112
119
|
}): Promise<CryptoKey>;
|
|
113
120
|
/**
|
|
114
121
|
* Wrap an AES key to a recipient's public key. `create(...)` calls this
|
|
@@ -118,20 +125,26 @@ declare function openBytes(keypair: CryptoKeyPair, wrapped: Uint8Array, opts?: {
|
|
|
118
125
|
* `CryptoKeyPair` (its `.publicKey` is used), 32 raw bytes (`Uint8Array`),
|
|
119
126
|
* or `{ publicKey:string, encoding? }` (encoding defaults to `base64url`).
|
|
120
127
|
* @param aesKey Optional key to wrap, as either an AES-GCM `CryptoKey` or its
|
|
121
|
-
* raw bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh
|
|
122
|
-
*
|
|
123
|
-
*
|
|
128
|
+
* raw bytes (`Uint8Array`, 16 or 32 bytes). Omit to generate a fresh key
|
|
129
|
+
* of `opts.size` bits. A supplied `CryptoKey` MUST be extractable (its
|
|
130
|
+
* raw bytes are wrapped) and is returned as-is: the same object, with
|
|
131
|
+
* its own `extractable` and usages. It does not need the `encrypt`
|
|
132
|
+
* usage, since wrapping never encrypts with it.
|
|
124
133
|
* @param opts `size` (128/256, default 256; ignored when `aesKey` is
|
|
125
|
-
* supplied)
|
|
126
|
-
*
|
|
127
|
-
*
|
|
134
|
+
* supplied), `info` (bound into the HPKE key schedule; default empty),
|
|
135
|
+
* and `extractable` (whether a key this function imports -- a
|
|
136
|
+
* generated key or raw `Uint8Array` bytes -- can be exported; default
|
|
137
|
+
* false). `extractable` does not apply to a supplied `CryptoKey`.
|
|
138
|
+
* @returns { wrapped:Uint8Array, key:CryptoKey } The wrapped envelope
|
|
139
|
+
* bytes and the AES-GCM `CryptoKey`: the supplied `CryptoKey` itself
|
|
140
|
+
* when one was given, otherwise a newly imported key.
|
|
128
141
|
*/
|
|
129
142
|
export declare function encryptKey(recipient: RecipientKey, aesKey?: CryptoKey | Uint8Array | null, opts?: {
|
|
130
143
|
size?: 128 | 256;
|
|
131
144
|
info?: Uint8Array | string;
|
|
145
|
+
extractable?: boolean;
|
|
132
146
|
}): Promise<{
|
|
133
|
-
|
|
147
|
+
wrapped: Uint8Array;
|
|
134
148
|
key: CryptoKey;
|
|
135
149
|
}>;
|
|
136
|
-
export {};
|
|
137
150
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,YAAY,EAGjB,eAAe,EACf,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,YAAY,EAGjB,eAAe,EACf,YAAY,EAOZ,KAAK,EACL,WAAW,EACd,MAAM,QAAQ,CAAA;AAYf,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAWxD;;;;;;;GAOG;AACH,wBAAsB,MAAM,CACxB,SAAS,EAAC,YAAY,EACtB,IAAI,CAAC,EAAC;IAEF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IAEb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;IAEvB,WAAW,CAAC,EAAC,OAAO,CAAA;CACvB,GACH,OAAO,CAAC;IAAE,OAAO,EAAC,UAAU,CAAC,eAAe,CAAC,CAAC;IAAC,GAAG,EAAC,SAAS,CAAA;CAAE,CAAC,CAGhE;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI;;CAEf,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,OAAO;;CAElB,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO;;;CAGlB,CAAA;AAEF;;;;;;;;GAQG;AACH,iBAAe,YAAY,CACvB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GACnC,OAAO,CAAC,UAAU,CAAC,CA+CpB;AAED;;;;;;;;GAQG;AACH,iBAAe,eAAe,CAC1B,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GACnC,OAAO,CAAC,MAAM,CAAC,CAGhB;AAED;;;;;;;;;;;;;GAaG;AACH,iBAAe,iBAAiB,CAC5B,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,MAAM,EACd,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAC;IAAC,MAAM,CAAC,EAAC,KAAK,CAAA;CAAE,GAClD,OAAO,CAAC,MAAM,CAAC,CAAA;AAEjB,iBAAe,iBAAiB,CAC5B,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,MAAM,EACd,IAAI,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAC;IAAC,MAAM,EAAC,IAAI,CAAA;CAAE,GAC/C,OAAO,CAAC,UAAU,CAAC,CAAA;AA6IrB;;;;;;;;;;GAUG;AACH,iBAAe,YAAY,CACvB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;CAAE,GACnC,OAAO,CAAC,UAAU,CAAC,CAepB;AAED;;;;;;;;;GASG;AACH,iBAAe,SAAS,CACpB,OAAO,EAAC,aAAa,EACrB,OAAO,EAAC,UAAU,EAClB,IAAI,CAAC,EAAC;IAAE,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAC;IAAC,WAAW,CAAC,EAAC,OAAO,CAAA;CAAE,GACzD,OAAO,CAAC,SAAS,CAAC,CAGnB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,UAAU,CAC5B,SAAS,EAAC,YAAY,EACtB,MAAM,CAAC,EAAC,SAAS,GAAC,UAAU,GAAC,IAAI,EACjC,IAAI,CAAC,EAAC;IAEF,IAAI,CAAC,EAAC,GAAG,GAAC,GAAG,CAAA;IAEb,IAAI,CAAC,EAAC,UAAU,GAAC,MAAM,CAAA;IAGvB,WAAW,CAAC,EAAC,OAAO,CAAA;CACvB,GACH,OAAO,CAAC;IAAE,OAAO,EAAC,UAAU,CAAC;IAAC,GAAG,EAAC,SAAS,CAAA;CAAE,CAAC,CA8C/C"}
|
package/dist/index.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/util.ts
|
|
1310
1325
|
var subtle = globalThis.crypto.subtle;
|
|
@@ -1332,24 +1347,44 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1332
1347
|
false,
|
|
1333
1348
|
["decrypt"]
|
|
1334
1349
|
);
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1350
|
+
let pt;
|
|
1351
|
+
try {
|
|
1352
|
+
pt = await subtle.decrypt(
|
|
1353
|
+
{ name: "AES-GCM", iv: nonce },
|
|
1354
|
+
k,
|
|
1355
|
+
ciphertext
|
|
1356
|
+
);
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
throw new HpkeError(
|
|
1359
|
+
"ERR_DECRYPT_FAILED",
|
|
1360
|
+
"failed to decrypt the wrapped key: wrong keypair, wrong info, or a tampered envelope",
|
|
1361
|
+
{ cause: err }
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1340
1364
|
return new Uint8Array(pt);
|
|
1341
1365
|
}
|
|
1342
1366
|
__name(aeadOpen, "aeadOpen");
|
|
1343
1367
|
async function encryptBytes(recipient, message, aesKey, opts) {
|
|
1368
|
+
if (aesKey instanceof CryptoKey && !aesKey.usages.includes("encrypt")) {
|
|
1369
|
+
throw new HpkeError(
|
|
1370
|
+
"ERR_INVALID_AES_KEY",
|
|
1371
|
+
'aesKey is missing the "encrypt" usage, which encrypt needs to encrypt the message'
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1344
1374
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1345
|
-
const {
|
|
1375
|
+
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1346
1376
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
1347
1377
|
const ct = new Uint8Array(await subtle.encrypt(
|
|
1348
1378
|
{ name: "AES-GCM", iv },
|
|
1349
1379
|
key,
|
|
1350
1380
|
plaintext
|
|
1351
1381
|
));
|
|
1352
|
-
return concat2(
|
|
1382
|
+
return concat2(
|
|
1383
|
+
i2osp(wrapped.length, WRAPPED_LEN_PREFIX),
|
|
1384
|
+
wrapped,
|
|
1385
|
+
iv,
|
|
1386
|
+
ct
|
|
1387
|
+
);
|
|
1353
1388
|
}
|
|
1354
1389
|
__name(encryptBytes, "encryptBytes");
|
|
1355
1390
|
async function encryptToString(recipient, message, aesKey, opts) {
|
|
@@ -1415,7 +1450,8 @@ async function exportRawPublic(key) {
|
|
|
1415
1450
|
__name(exportRawPublic, "exportRawPublic");
|
|
1416
1451
|
async function importRawPublic(raw) {
|
|
1417
1452
|
if (raw.length !== ENC_LENGTH) {
|
|
1418
|
-
throw new
|
|
1453
|
+
throw new HpkeError(
|
|
1454
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1419
1455
|
`invalid public key length: ${raw.length} bytes (expected ${ENC_LENGTH})`
|
|
1420
1456
|
);
|
|
1421
1457
|
}
|
|
@@ -1433,44 +1469,83 @@ async function resolveRecipientPublicKey(recipient) {
|
|
|
1433
1469
|
return importRawPublic(recipient);
|
|
1434
1470
|
}
|
|
1435
1471
|
if (recipient instanceof CryptoKey) {
|
|
1436
|
-
|
|
1437
|
-
throw new Error("recipient CryptoKey must be a public key");
|
|
1438
|
-
}
|
|
1439
|
-
assertX25519Algorithm(recipient, "recipient CryptoKey");
|
|
1440
|
-
return recipient;
|
|
1472
|
+
return assertRecipientPublicKey(recipient, "recipient CryptoKey");
|
|
1441
1473
|
}
|
|
1442
1474
|
if (typeof recipient.publicKey === "string") {
|
|
1443
1475
|
const { publicKey, encoding } = recipient;
|
|
1444
|
-
|
|
1476
|
+
let decoded;
|
|
1477
|
+
try {
|
|
1478
|
+
decoded = fromString2(publicKey, encoding ?? "base64url");
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
throw new HpkeError(
|
|
1481
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1482
|
+
`recipient publicKey string does not decode as ${encoding ?? "base64url"}`,
|
|
1483
|
+
{ cause: err }
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
return importRawPublic(decoded);
|
|
1445
1487
|
}
|
|
1446
1488
|
const pair = recipient;
|
|
1447
1489
|
if (pair.publicKey instanceof CryptoKey) {
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
return pair.publicKey;
|
|
1490
|
+
return assertRecipientPublicKey(
|
|
1491
|
+
pair.publicKey,
|
|
1492
|
+
"recipient keypair publicKey"
|
|
1493
|
+
);
|
|
1453
1494
|
}
|
|
1454
|
-
throw new
|
|
1495
|
+
throw new HpkeError(
|
|
1496
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1497
|
+
"unrecognized recipient key form"
|
|
1498
|
+
);
|
|
1455
1499
|
}
|
|
1456
1500
|
__name(resolveRecipientPublicKey, "resolveRecipientPublicKey");
|
|
1457
|
-
function
|
|
1501
|
+
function assertRecipientPublicKey(key, context) {
|
|
1502
|
+
if (key.type !== "public") {
|
|
1503
|
+
throw new HpkeError(
|
|
1504
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1505
|
+
`${context} must be a public key`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
assertX25519Algorithm(key, context, "ERR_INVALID_RECIPIENT_KEY");
|
|
1509
|
+
if (!key.extractable) {
|
|
1510
|
+
throw new HpkeError(
|
|
1511
|
+
"ERR_INVALID_RECIPIENT_KEY",
|
|
1512
|
+
`${context} must be extractable (the KEM exports it)`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
return key;
|
|
1516
|
+
}
|
|
1517
|
+
__name(assertRecipientPublicKey, "assertRecipientPublicKey");
|
|
1518
|
+
function assertX25519Algorithm(key, context, code2) {
|
|
1458
1519
|
if (key.algorithm.name !== "X25519") {
|
|
1459
|
-
throw new
|
|
1520
|
+
throw new HpkeError(
|
|
1521
|
+
code2,
|
|
1460
1522
|
`${context} must be an X25519 key (got ${key.algorithm.name})`
|
|
1461
1523
|
);
|
|
1462
1524
|
}
|
|
1463
1525
|
}
|
|
1464
1526
|
__name(assertX25519Algorithm, "assertX25519Algorithm");
|
|
1465
1527
|
async function dh(priv, pub) {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1528
|
+
let bits;
|
|
1529
|
+
try {
|
|
1530
|
+
bits = await subtle.deriveBits(
|
|
1531
|
+
{ name: "X25519", public: pub },
|
|
1532
|
+
priv,
|
|
1533
|
+
256
|
|
1534
|
+
);
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
if (err?.name === "OperationError") {
|
|
1537
|
+
throw new HpkeError(
|
|
1538
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1539
|
+
"X25519 deriveBits rejected a small-order public key",
|
|
1540
|
+
{ cause: err }
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
throw err;
|
|
1544
|
+
}
|
|
1471
1545
|
const bytes = new Uint8Array(bits);
|
|
1472
1546
|
if (bytes.every((b) => b === 0)) {
|
|
1473
|
-
throw new
|
|
1547
|
+
throw new HpkeError(
|
|
1548
|
+
"ERR_SMALL_ORDER_KEY",
|
|
1474
1549
|
"X25519 shared secret is all-zero (small-order public key?)"
|
|
1475
1550
|
);
|
|
1476
1551
|
}
|
|
@@ -1509,9 +1584,33 @@ async function encap(pkR) {
|
|
|
1509
1584
|
__name(encap, "encap");
|
|
1510
1585
|
async function decap(enc, keypair) {
|
|
1511
1586
|
if (keypair.privateKey.type !== "private") {
|
|
1512
|
-
throw new
|
|
1587
|
+
throw new HpkeError(
|
|
1588
|
+
"ERR_INVALID_KEYPAIR",
|
|
1589
|
+
"keypair.privateKey must be a private key"
|
|
1590
|
+
);
|
|
1513
1591
|
}
|
|
1514
|
-
assertX25519Algorithm(
|
|
1592
|
+
assertX25519Algorithm(
|
|
1593
|
+
keypair.privateKey,
|
|
1594
|
+
"keypair.privateKey",
|
|
1595
|
+
"ERR_INVALID_KEYPAIR"
|
|
1596
|
+
);
|
|
1597
|
+
if (!keypair.privateKey.usages.includes("deriveBits")) {
|
|
1598
|
+
throw new HpkeError(
|
|
1599
|
+
"ERR_INVALID_KEYPAIR",
|
|
1600
|
+
"keypair.privateKey must have the deriveBits usage"
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
if (keypair.publicKey.type !== "public" || !keypair.publicKey.extractable) {
|
|
1604
|
+
throw new HpkeError(
|
|
1605
|
+
"ERR_INVALID_KEYPAIR",
|
|
1606
|
+
"keypair.publicKey must be an extractable public key"
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
assertX25519Algorithm(
|
|
1610
|
+
keypair.publicKey,
|
|
1611
|
+
"keypair.publicKey",
|
|
1612
|
+
"ERR_INVALID_KEYPAIR"
|
|
1613
|
+
);
|
|
1515
1614
|
const pkE = await subtle.importKey(
|
|
1516
1615
|
"raw",
|
|
1517
1616
|
enc,
|
|
@@ -1525,6 +1624,45 @@ async function decap(enc, keypair) {
|
|
|
1525
1624
|
return extractAndExpand(dhBytes, kemContext);
|
|
1526
1625
|
}
|
|
1527
1626
|
__name(decap, "decap");
|
|
1627
|
+
async function keyId(key) {
|
|
1628
|
+
if (key.algorithm.name !== "AES-GCM") {
|
|
1629
|
+
throw new HpkeError(
|
|
1630
|
+
"ERR_INVALID_AES_KEY",
|
|
1631
|
+
`keyId needs an AES-GCM key (got ${key.algorithm.name})`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (!key.usages.includes("encrypt")) {
|
|
1635
|
+
throw new HpkeError(
|
|
1636
|
+
"ERR_INVALID_AES_KEY",
|
|
1637
|
+
"keyId needs a key with the 'encrypt' usage"
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const labelHash = new Uint8Array(
|
|
1641
|
+
await subtle.digest("SHA-256", KEY_ID_LABEL)
|
|
1642
|
+
);
|
|
1643
|
+
const nonce = labelHash.slice(0, KEY_ID_NONCE_LENGTH);
|
|
1644
|
+
const ciphertext = new Uint8Array(await subtle.encrypt(
|
|
1645
|
+
{ name: "AES-GCM", iv: nonce },
|
|
1646
|
+
key,
|
|
1647
|
+
KEY_ID_LABEL
|
|
1648
|
+
));
|
|
1649
|
+
const digest2 = await subtle.digest(
|
|
1650
|
+
"SHA-256",
|
|
1651
|
+
concat2(KEY_ID_LABEL, ciphertext)
|
|
1652
|
+
);
|
|
1653
|
+
return toString2(new Uint8Array(digest2), "base64url");
|
|
1654
|
+
}
|
|
1655
|
+
__name(keyId, "keyId");
|
|
1656
|
+
async function verifyKeyId(key, expected) {
|
|
1657
|
+
const actual = await keyId(key);
|
|
1658
|
+
if (actual.length !== expected.length) return false;
|
|
1659
|
+
let diff = 0;
|
|
1660
|
+
for (let i = 0; i < actual.length; i++) {
|
|
1661
|
+
diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
1662
|
+
}
|
|
1663
|
+
return diff === 0;
|
|
1664
|
+
}
|
|
1665
|
+
__name(verifyKeyId, "verifyKeyId");
|
|
1528
1666
|
function i2osp(n, len) {
|
|
1529
1667
|
const out = new Uint8Array(len);
|
|
1530
1668
|
let v = n;
|
|
@@ -1551,8 +1689,8 @@ __name(concat2, "concat");
|
|
|
1551
1689
|
// src/index.ts
|
|
1552
1690
|
var subtle2 = globalThis.crypto.subtle;
|
|
1553
1691
|
async function create2(recipient, opts) {
|
|
1554
|
-
const {
|
|
1555
|
-
return {
|
|
1692
|
+
const { wrapped, key } = await encryptKey(recipient, null, opts);
|
|
1693
|
+
return { wrapped, key };
|
|
1556
1694
|
}
|
|
1557
1695
|
__name(create2, "create");
|
|
1558
1696
|
var open = Object.assign(openBytes, {
|
|
@@ -1567,13 +1705,19 @@ var decrypt = Object.assign(decryptBytes, {
|
|
|
1567
1705
|
});
|
|
1568
1706
|
async function decryptBytes(keypair, message, opts) {
|
|
1569
1707
|
if (message.byteLength < WRAPPED_LEN_PREFIX) {
|
|
1570
|
-
throw new
|
|
1708
|
+
throw new HpkeError(
|
|
1709
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1710
|
+
"malformed message: too short"
|
|
1711
|
+
);
|
|
1571
1712
|
}
|
|
1572
1713
|
const wrappedLen = message[0] << 8 | message[1];
|
|
1573
1714
|
const ivStart = WRAPPED_LEN_PREFIX + wrappedLen;
|
|
1574
1715
|
const ctStart = ivStart + NN;
|
|
1575
1716
|
if (message.byteLength < ctStart + AEAD_TAG_LENGTH) {
|
|
1576
|
-
throw new
|
|
1717
|
+
throw new HpkeError(
|
|
1718
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1719
|
+
"malformed message: too short"
|
|
1720
|
+
);
|
|
1577
1721
|
}
|
|
1578
1722
|
const wrapped = message.slice(WRAPPED_LEN_PREFIX, ivStart);
|
|
1579
1723
|
const iv = message.slice(ivStart, ctStart);
|
|
@@ -1584,11 +1728,20 @@ async function decryptBytes(keypair, message, opts) {
|
|
|
1584
1728
|
opts?.info !== void 0 ? { info: opts.info } : void 0
|
|
1585
1729
|
);
|
|
1586
1730
|
const key = await importAesKeyDecryptOnly(keyBytes);
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1731
|
+
let pt;
|
|
1732
|
+
try {
|
|
1733
|
+
pt = await subtle2.decrypt(
|
|
1734
|
+
{ name: "AES-GCM", iv },
|
|
1735
|
+
key,
|
|
1736
|
+
ciphertext
|
|
1737
|
+
);
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
throw new HpkeError(
|
|
1740
|
+
"ERR_DECRYPT_FAILED",
|
|
1741
|
+
"failed to decrypt the message: tampered ciphertext",
|
|
1742
|
+
{ cause: err }
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1592
1745
|
return new Uint8Array(pt);
|
|
1593
1746
|
}
|
|
1594
1747
|
__name(decryptBytes, "decryptBytes");
|
|
@@ -1598,7 +1751,16 @@ async function decryptToString(keypair, message, opts) {
|
|
|
1598
1751
|
}
|
|
1599
1752
|
__name(decryptToString, "decryptToString");
|
|
1600
1753
|
async function decryptFromString(keypair, message, opts) {
|
|
1601
|
-
|
|
1754
|
+
let bytes;
|
|
1755
|
+
try {
|
|
1756
|
+
bytes = fromString2(message, "base64url");
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
throw new HpkeError(
|
|
1759
|
+
"ERR_MALFORMED_MESSAGE",
|
|
1760
|
+
"malformed message: not base64url",
|
|
1761
|
+
{ cause: err }
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1602
1764
|
const plaintext = await decryptBytes(
|
|
1603
1765
|
keypair,
|
|
1604
1766
|
bytes,
|
|
@@ -1609,7 +1771,8 @@ async function decryptFromString(keypair, message, opts) {
|
|
|
1609
1771
|
__name(decryptFromString, "decryptFromString");
|
|
1610
1772
|
function validateKeysize(keysize) {
|
|
1611
1773
|
if (keysize !== 128 && keysize !== 256) {
|
|
1612
|
-
throw new
|
|
1774
|
+
throw new HpkeError(
|
|
1775
|
+
"ERR_INVALID_KEYSIZE",
|
|
1613
1776
|
`invalid keysize: ${keysize} (expected 128 or 256)`
|
|
1614
1777
|
);
|
|
1615
1778
|
}
|
|
@@ -1617,7 +1780,8 @@ function validateKeysize(keysize) {
|
|
|
1617
1780
|
__name(validateKeysize, "validateKeysize");
|
|
1618
1781
|
function validateRawKeyBytes(raw) {
|
|
1619
1782
|
if (raw.length !== 16 && raw.length !== 32) {
|
|
1620
|
-
throw new
|
|
1783
|
+
throw new HpkeError(
|
|
1784
|
+
"ERR_INVALID_AES_KEY",
|
|
1621
1785
|
`invalid aesKey length: ${raw.length} bytes (expected 16 or 32)`
|
|
1622
1786
|
);
|
|
1623
1787
|
}
|
|
@@ -1631,19 +1795,20 @@ function normalizeInfo(info) {
|
|
|
1631
1795
|
__name(normalizeInfo, "normalizeInfo");
|
|
1632
1796
|
async function exportAesKeyBytes(key) {
|
|
1633
1797
|
if (!key.extractable) {
|
|
1634
|
-
throw new
|
|
1798
|
+
throw new HpkeError(
|
|
1799
|
+
"ERR_INVALID_AES_KEY",
|
|
1635
1800
|
"aesKey must be extractable: its raw bytes are what get sealed"
|
|
1636
1801
|
);
|
|
1637
1802
|
}
|
|
1638
1803
|
return new Uint8Array(await subtle2.exportKey("raw", key));
|
|
1639
1804
|
}
|
|
1640
1805
|
__name(exportAesKeyBytes, "exportAesKeyBytes");
|
|
1641
|
-
async function importAesKey(raw) {
|
|
1806
|
+
async function importAesKey(raw, extractable) {
|
|
1642
1807
|
return subtle2.importKey(
|
|
1643
1808
|
"raw",
|
|
1644
1809
|
raw,
|
|
1645
1810
|
{ name: "AES-GCM" },
|
|
1646
|
-
|
|
1811
|
+
extractable,
|
|
1647
1812
|
["encrypt", "decrypt"]
|
|
1648
1813
|
);
|
|
1649
1814
|
}
|
|
@@ -1698,7 +1863,10 @@ async function keySchedule(sharedSecret, info) {
|
|
|
1698
1863
|
__name(keySchedule, "keySchedule");
|
|
1699
1864
|
async function openRawBytes(keypair, wrapped, opts) {
|
|
1700
1865
|
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1701
|
-
throw new
|
|
1866
|
+
throw new HpkeError(
|
|
1867
|
+
"ERR_MALFORMED_ENVELOPE",
|
|
1868
|
+
"malformed envelope: too short"
|
|
1869
|
+
);
|
|
1702
1870
|
}
|
|
1703
1871
|
const info = normalizeInfo(opts?.info);
|
|
1704
1872
|
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
@@ -1710,7 +1878,7 @@ async function openRawBytes(keypair, wrapped, opts) {
|
|
|
1710
1878
|
__name(openRawBytes, "openRawBytes");
|
|
1711
1879
|
async function openBytes(keypair, wrapped, opts) {
|
|
1712
1880
|
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1713
|
-
return importAesKey(keyBytes);
|
|
1881
|
+
return importAesKey(keyBytes, opts?.extractable ?? false);
|
|
1714
1882
|
}
|
|
1715
1883
|
__name(openBytes, "openBytes");
|
|
1716
1884
|
async function encryptKey(recipient, aesKey, opts) {
|
|
@@ -1720,6 +1888,12 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1720
1888
|
validateRawKeyBytes(aesKey);
|
|
1721
1889
|
keyBytes = aesKey;
|
|
1722
1890
|
} else if (aesKey) {
|
|
1891
|
+
if (aesKey.algorithm.name !== "AES-GCM") {
|
|
1892
|
+
throw new HpkeError(
|
|
1893
|
+
"ERR_INVALID_AES_KEY",
|
|
1894
|
+
"aesKey must be an AES-GCM key, got " + aesKey.algorithm.name
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1723
1897
|
keyBytes = await exportAesKeyBytes(aesKey);
|
|
1724
1898
|
validateRawKeyBytes(keyBytes);
|
|
1725
1899
|
} else {
|
|
@@ -1734,15 +1908,22 @@ async function encryptKey(recipient, aesKey, opts) {
|
|
|
1734
1908
|
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1735
1909
|
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1736
1910
|
const wrapped = concat2(enc, ciphertext);
|
|
1737
|
-
|
|
1738
|
-
|
|
1911
|
+
if (aesKey instanceof CryptoKey) return { wrapped, key: aesKey };
|
|
1912
|
+
const aesGcmKey = await importAesKey(
|
|
1913
|
+
keyBytes,
|
|
1914
|
+
opts?.extractable ?? false
|
|
1915
|
+
);
|
|
1916
|
+
return { wrapped, key: aesGcmKey };
|
|
1739
1917
|
}
|
|
1740
1918
|
__name(encryptKey, "encryptKey");
|
|
1741
1919
|
export {
|
|
1920
|
+
HpkeError,
|
|
1742
1921
|
create2 as create,
|
|
1743
1922
|
decrypt,
|
|
1744
1923
|
encrypt,
|
|
1745
1924
|
encryptKey,
|
|
1746
|
-
|
|
1925
|
+
keyId,
|
|
1926
|
+
open,
|
|
1927
|
+
verifyKeyId
|
|
1747
1928
|
};
|
|
1748
1929
|
//# sourceMappingURL=index.js.map
|