simple-hpke 0.0.11 → 0.1.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 +35 -29
- package/dist/constants.cjs +61 -41
- package/dist/constants.cjs.map +3 -3
- package/dist/constants.js +61 -41
- package/dist/constants.js.map +3 -3
- package/dist/constants.min.cjs +1 -1
- package/dist/constants.min.cjs.map +3 -3
- package/dist/constants.min.js +1 -1
- package/dist/constants.min.js.map +3 -3
- package/dist/index.cjs +62 -41
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +46 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -41
- package/dist/index.js.map +3 -3
- package/dist/index.min.cjs +1 -1
- package/dist/index.min.cjs.map +3 -3
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +3 -3
- package/dist/meta.json +15 -14
- package/dist/util.cjs +61 -41
- package/dist/util.cjs.map +3 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +61 -41
- package/dist/util.js.map +3 -3
- package/dist/util.min.cjs +1 -1
- package/dist/util.min.cjs.map +3 -3
- package/dist/util.min.js +1 -1
- package/dist/util.min.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Simple HPKE
|
|
2
|
-
[](https://github.com/vanishing-page/simple-hpke/actions/workflows/nodejs.yml)
|
|
3
3
|
[](README.md)
|
|
4
4
|
[](README.md)
|
|
5
5
|
[](https://semver.org/)
|
|
@@ -54,7 +54,7 @@ Wrap an AES key, or encrypt a message.
|
|
|
54
54
|
Encrypt an AES key to yourself, then recover it later.
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
|
-
import { seal, open } from 'simple-hpke'
|
|
57
|
+
import { create, seal, open } from 'simple-hpke'
|
|
58
58
|
|
|
59
59
|
// An X25519 keypair. (asymmetric keypair).
|
|
60
60
|
// The private key can be non-extractable.
|
|
@@ -65,8 +65,8 @@ const keypair = await crypto.subtle.generateKey(
|
|
|
65
65
|
['deriveBits']
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
const {
|
|
68
|
+
// Create a new AES key and encrypt it to your public key.
|
|
69
|
+
const { enc, key } = await create(keypair)
|
|
70
70
|
|
|
71
71
|
// Or wrap an existing AES key. The supplied AES key must be extractable.
|
|
72
72
|
const aesKey = await crypto.subtle.generateKey(
|
|
@@ -75,11 +75,13 @@ const aesKey = await crypto.subtle.generateKey(
|
|
|
75
75
|
['encrypt', 'decrypt']
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
const {
|
|
78
|
+
// Wrap an existing key.
|
|
79
|
+
const { enc: sealedKey } = await seal(keypair, aesKey)
|
|
80
|
+
|
|
81
|
+
// or pass in just a public key
|
|
80
82
|
|
|
81
83
|
// Later, recover the same key with your private key.
|
|
82
|
-
const recoveredKey = await open(keypair,
|
|
84
|
+
const recoveredKey = await open(keypair, sealedKey)
|
|
83
85
|
|
|
84
86
|
// `recoveredKey` is equal to `aesKey`
|
|
85
87
|
```
|
|
@@ -98,7 +100,7 @@ along with the IV. The recipient uses their private key to open the AES key and
|
|
|
98
100
|
decrypt the message.
|
|
99
101
|
|
|
100
102
|
```ts
|
|
101
|
-
import {
|
|
103
|
+
import { create, open } from 'simple-hpke'
|
|
102
104
|
|
|
103
105
|
const recipient = await crypto.subtle.generateKey(
|
|
104
106
|
{ name: 'X25519' },
|
|
@@ -106,31 +108,34 @@ const recipient = await crypto.subtle.generateKey(
|
|
|
106
108
|
['deriveBits']
|
|
107
109
|
)
|
|
108
110
|
|
|
109
|
-
// Create
|
|
110
|
-
const {
|
|
111
|
+
// Create a fresh AES-GCM key, and encrypt a message with it.
|
|
112
|
+
const { enc, key } = await create(recipient)
|
|
111
113
|
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
112
|
-
const
|
|
114
|
+
const ciphertextBuffer = await crypto.subtle.encrypt(
|
|
113
115
|
{ name: 'AES-GCM', iv },
|
|
114
116
|
key,
|
|
115
117
|
new TextEncoder().encode('attack at dawn')
|
|
116
118
|
)
|
|
117
119
|
|
|
118
|
-
// Send `
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
message
|
|
120
|
+
// Send `enc`, `iv`, and `ciphertext` together. `create(...)` defaults to a
|
|
121
|
+
// 256-bit AES key, so `enc.length` is 80 bytes in this example.
|
|
122
|
+
//
|
|
123
|
+
// If your protocol allows different wrapped-key sizes, prefix `enc.length`
|
|
124
|
+
// (or otherwise transmit it) so the recipient can split the payload safely.
|
|
125
|
+
const ciphertext = new Uint8Array(ciphertextBuffer)
|
|
126
|
+
const encLength = enc.length
|
|
127
|
+
const message = new Uint8Array(encLength + iv.length + ciphertext.length)
|
|
128
|
+
message.set(enc, 0)
|
|
129
|
+
message.set(iv, encLength)
|
|
130
|
+
message.set(ciphertext, encLength + iv.length)
|
|
126
131
|
|
|
127
132
|
// On the other side, split the payload back into its parts.
|
|
128
|
-
const
|
|
129
|
-
const iv2 = message.subarray(
|
|
130
|
-
const ciphertext2 = message.subarray(
|
|
133
|
+
const enc2 = message.subarray(0, encLength)
|
|
134
|
+
const iv2 = message.subarray(encLength, encLength + 12)
|
|
135
|
+
const ciphertext2 = message.subarray(encLength + 12)
|
|
131
136
|
|
|
132
137
|
// Recover the key, then decrypt the message.
|
|
133
|
-
const recovered = await open(recipient,
|
|
138
|
+
const recovered = await open(recipient, enc2)
|
|
134
139
|
const plaintext = await crypto.subtle.decrypt(
|
|
135
140
|
{ name: 'AES-GCM', iv: iv2 },
|
|
136
141
|
recovered,
|
|
@@ -146,7 +151,8 @@ So that was a lot of code to encrypt and decrypt a message...
|
|
|
146
151
|
This package exposes functions `encrypt` and `decrypt` which do the same thing.
|
|
147
152
|
|
|
148
153
|
`encrypt` seals an AES key to the recipient, encrypts the message under
|
|
149
|
-
that key, and returns a single envelope:
|
|
154
|
+
that key, and returns a single envelope:
|
|
155
|
+
`wrappedLen + wrapped + iv + ciphertext`
|
|
150
156
|
(a 2-byte length prefix, the wrapped key, the 12-byte AES-GCM IV, and the
|
|
151
157
|
cipher text). `decrypt` reverses it, returning the plaintext bytes.
|
|
152
158
|
|
|
@@ -156,8 +162,8 @@ cipher text). `decrypt` reverses it, returning the plaintext bytes.
|
|
|
156
162
|
> [`decrypt.fromString`](#decryptfromstring) below for a convenient way
|
|
157
163
|
> to decrypt from a string.
|
|
158
164
|
>
|
|
159
|
-
> See [`encrypt.asString`](#
|
|
160
|
-
>
|
|
165
|
+
> See [`encrypt.asString`](#encryptasstring) for encrypting and encoding to a
|
|
166
|
+
> string.
|
|
161
167
|
>
|
|
162
168
|
|
|
163
169
|
|
|
@@ -249,7 +255,7 @@ bytes to `decrypt` / `decrypt.asString`.
|
|
|
249
255
|
|
|
250
256
|
##### `decrypt`
|
|
251
257
|
|
|
252
|
-
|
|
258
|
+
Decrypt the given data, return a `Uint8Array`.
|
|
253
259
|
|
|
254
260
|
```ts
|
|
255
261
|
async function decrypt (
|
|
@@ -261,7 +267,7 @@ async function decrypt (
|
|
|
261
267
|
|
|
262
268
|
##### `decrypt.asString`
|
|
263
269
|
|
|
264
|
-
Take a `Uint8Array`, return a
|
|
270
|
+
Take a `Uint8Array`, return a string.
|
|
265
271
|
|
|
266
272
|
```ts
|
|
267
273
|
decrypt.asString = async function decryptToString (
|
|
@@ -293,7 +299,7 @@ This exposes ESM and common JS via
|
|
|
293
299
|
|
|
294
300
|
### ESM
|
|
295
301
|
```js
|
|
296
|
-
import { seal, open, encrypt, decrypt } from 'simple-hpke'
|
|
302
|
+
import { create, seal, open, encrypt, decrypt } from 'simple-hpke'
|
|
297
303
|
```
|
|
298
304
|
|
|
299
305
|
### Common JS
|
package/dist/constants.cjs
CHANGED
|
@@ -1317,51 +1317,19 @@ __name(toString2, "toString");
|
|
|
1317
1317
|
|
|
1318
1318
|
// src/index.ts
|
|
1319
1319
|
var subtle = globalThis.crypto.subtle;
|
|
1320
|
+
async function create2(recipient, opts) {
|
|
1321
|
+
const { wrapped, key } = await encryptKey(recipient, null, opts);
|
|
1322
|
+
return { enc: wrapped, key };
|
|
1323
|
+
}
|
|
1324
|
+
__name(create2, "create");
|
|
1320
1325
|
async function seal(recipient, aesKey, opts) {
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
if (aesKey instanceof Uint8Array) {
|
|
1324
|
-
validateRawKeyBytes(aesKey);
|
|
1325
|
-
keyBytes = aesKey;
|
|
1326
|
-
} else if (aesKey) {
|
|
1327
|
-
keyBytes = await exportAesKeyBytes(aesKey);
|
|
1328
|
-
validateRawKeyBytes(keyBytes);
|
|
1329
|
-
} else {
|
|
1330
|
-
const keysize = opts?.keysize ?? 256;
|
|
1331
|
-
validateKeysize(keysize);
|
|
1332
|
-
keyBytes = globalThis.crypto.getRandomValues(
|
|
1333
|
-
new Uint8Array(keysize / 8)
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
const publicKey = await resolveRecipientPublicKey(recipient);
|
|
1337
|
-
const { sharedSecret, enc } = await encap(publicKey);
|
|
1338
|
-
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1339
|
-
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1340
|
-
const wrapped = concat2(enc, ciphertext);
|
|
1341
|
-
const aesGcmKey = await importAesKey(keyBytes);
|
|
1342
|
-
return { wrapped, key: aesGcmKey };
|
|
1326
|
+
const { wrapped, key } = await encryptKey(recipient, aesKey, opts);
|
|
1327
|
+
return { enc: wrapped, key };
|
|
1343
1328
|
}
|
|
1344
1329
|
__name(seal, "seal");
|
|
1345
1330
|
var open = Object.assign(openBytes, {
|
|
1346
1331
|
raw: openRawBytes
|
|
1347
1332
|
});
|
|
1348
|
-
async function openBytes(keypair, wrapped, opts) {
|
|
1349
|
-
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1350
|
-
return importAesKey(keyBytes);
|
|
1351
|
-
}
|
|
1352
|
-
__name(openBytes, "openBytes");
|
|
1353
|
-
async function openRawBytes(keypair, wrapped, opts) {
|
|
1354
|
-
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1355
|
-
throw new Error("malformed envelope: too short");
|
|
1356
|
-
}
|
|
1357
|
-
const info = normalizeInfo(opts?.info);
|
|
1358
|
-
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
1359
|
-
const ciphertext = wrapped.slice(ENC_LENGTH);
|
|
1360
|
-
const sharedSecret = await decap(enc, keypair);
|
|
1361
|
-
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1362
|
-
return aeadOpen(key, baseNonce, ciphertext);
|
|
1363
|
-
}
|
|
1364
|
-
__name(openRawBytes, "openRawBytes");
|
|
1365
1333
|
var encrypt = Object.assign(encryptBytes, {
|
|
1366
1334
|
asString: encryptToString
|
|
1367
1335
|
});
|
|
@@ -1500,6 +1468,48 @@ async function keySchedule(sharedSecret, info) {
|
|
|
1500
1468
|
return { key, baseNonce };
|
|
1501
1469
|
}
|
|
1502
1470
|
__name(keySchedule, "keySchedule");
|
|
1471
|
+
async function openRawBytes(keypair, wrapped, opts) {
|
|
1472
|
+
if (wrapped.byteLength < ENC_LENGTH + AEAD_TAG_LENGTH) {
|
|
1473
|
+
throw new Error("malformed envelope: too short");
|
|
1474
|
+
}
|
|
1475
|
+
const info = normalizeInfo(opts?.info);
|
|
1476
|
+
const enc = wrapped.slice(0, ENC_LENGTH);
|
|
1477
|
+
const ciphertext = wrapped.slice(ENC_LENGTH);
|
|
1478
|
+
const sharedSecret = await decap(enc, keypair);
|
|
1479
|
+
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1480
|
+
return aeadOpen(key, baseNonce, ciphertext);
|
|
1481
|
+
}
|
|
1482
|
+
__name(openRawBytes, "openRawBytes");
|
|
1483
|
+
async function openBytes(keypair, wrapped, opts) {
|
|
1484
|
+
const keyBytes = await openRawBytes(keypair, wrapped, opts);
|
|
1485
|
+
return importAesKey(keyBytes);
|
|
1486
|
+
}
|
|
1487
|
+
__name(openBytes, "openBytes");
|
|
1488
|
+
async function encryptKey(recipient, aesKey, opts) {
|
|
1489
|
+
const info = normalizeInfo(opts?.info);
|
|
1490
|
+
let keyBytes;
|
|
1491
|
+
if (aesKey instanceof Uint8Array) {
|
|
1492
|
+
validateRawKeyBytes(aesKey);
|
|
1493
|
+
keyBytes = aesKey;
|
|
1494
|
+
} else if (aesKey) {
|
|
1495
|
+
keyBytes = await exportAesKeyBytes(aesKey);
|
|
1496
|
+
validateRawKeyBytes(keyBytes);
|
|
1497
|
+
} else {
|
|
1498
|
+
const keysize = opts?.size ?? 256;
|
|
1499
|
+
validateKeysize(keysize);
|
|
1500
|
+
keyBytes = globalThis.crypto.getRandomValues(
|
|
1501
|
+
new Uint8Array(keysize / 8)
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const publicKey = await resolveRecipientPublicKey(recipient);
|
|
1505
|
+
const { sharedSecret, enc } = await encap(publicKey);
|
|
1506
|
+
const { key, baseNonce } = await keySchedule(sharedSecret, info);
|
|
1507
|
+
const ciphertext = await aeadSeal(key, baseNonce, keyBytes);
|
|
1508
|
+
const wrapped = concat2(enc, ciphertext);
|
|
1509
|
+
const aesGcmKey = await importAesKey(keyBytes);
|
|
1510
|
+
return { wrapped, key: aesGcmKey };
|
|
1511
|
+
}
|
|
1512
|
+
__name(encryptKey, "encryptKey");
|
|
1503
1513
|
|
|
1504
1514
|
// src/util.ts
|
|
1505
1515
|
var subtle2 = globalThis.crypto.subtle;
|
|
@@ -1537,14 +1547,24 @@ async function aeadOpen(key, nonce, ciphertext) {
|
|
|
1537
1547
|
__name(aeadOpen, "aeadOpen");
|
|
1538
1548
|
async function encryptBytes(recipient, message, aesKey, opts) {
|
|
1539
1549
|
const plaintext = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1540
|
-
|
|
1550
|
+
let enc;
|
|
1551
|
+
let key;
|
|
1552
|
+
if (!aesKey) {
|
|
1553
|
+
const keys = await create2(recipient, opts);
|
|
1554
|
+
enc = keys.enc;
|
|
1555
|
+
key = keys.key;
|
|
1556
|
+
} else {
|
|
1557
|
+
const keys = await seal(recipient, aesKey, opts);
|
|
1558
|
+
enc = keys.enc;
|
|
1559
|
+
key = keys.key;
|
|
1560
|
+
}
|
|
1541
1561
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(NN));
|
|
1542
1562
|
const ct = new Uint8Array(await subtle2.encrypt(
|
|
1543
1563
|
{ name: "AES-GCM", iv },
|
|
1544
1564
|
key,
|
|
1545
1565
|
plaintext
|
|
1546
1566
|
));
|
|
1547
|
-
return concat2(i2osp(
|
|
1567
|
+
return concat2(i2osp(enc.length, WRAPPED_LEN_PREFIX), enc, iv, ct);
|
|
1548
1568
|
}
|
|
1549
1569
|
__name(encryptBytes, "encryptBytes");
|
|
1550
1570
|
async function encryptToString(recipient, message, aesKey, opts) {
|