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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # Simple HPKE
2
- [![tests](https://img.shields.io/github/actions/workflow/status/nichoth/simple-hpke/nodejs.yml?style=flat-square)](https://github.com/nichoth/simple-hpke/actions/workflows/nodejs.yml)
2
+ [![tests](https://img.shields.io/github/actions/workflow/status/vanishing-page/simple-hpke/nodejs.yml?style=flat-square)](https://github.com/vanishing-page/simple-hpke/actions/workflows/nodejs.yml)
3
3
  [![types](https://img.shields.io/npm/types/simple-hpke?style=flat-square)](README.md)
4
4
  [![module](https://img.shields.io/badge/module-ESM%2FCJS-blue?style=flat-square)](README.md)
5
5
  [![semantic versioning](https://img.shields.io/badge/semver-2.0.0-blue?logo=semver&style=flat-square)](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
- // create a new AES key, and encrypt it to your public key.
69
- const { wrapped, key } = await seal(keypair)
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
- // pass in an existing key. The return value has the wrapped key.
79
- const { wrapped } = await seal(keypair, aesKey)
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, wrapped)
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 { seal, open } from 'simple-hpke'
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 and seal a fresh AES-GCM key, and encrypt a message with it.
110
- const { wrapped, key } = await seal(recipient)
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 ciphertext = await crypto.subtle.encrypt(
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 `wrapped`, `iv`, and `ciphertext` together. `wrapped` is a fixed
119
- // 80 bytes for this suite, and the AES-GCM IV is 12 bytes, so the recipient
120
- // can slice the payload back apart at known offsets.
121
- const ciphertext = new Uint8Array(ciphertext)
122
- const message = new Uint8Array(wrapped.length + iv.length + ciphertext.length)
123
- message.set(wrapped, 0)
124
- message.set(iv, wrapped.length)
125
- message.set(ciphertext, wrapped.length + iv.length)
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 wrapped2 = message.subarray(0, 80)
129
- const iv2 = message.subarray(80, 80 + 12)
130
- const ciphertext2 = message.subarray(80 + 12)
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, wrapped2)
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: `wrappedLen + wrapped + iv + ciphertext`
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`](#encrypt-to-a-string) for encrypting and ecoding to
160
- > a string.
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
- Descrypt the given data, return a `Uint8Array`.
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 sttring.
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
@@ -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 info = normalizeInfo(opts?.info);
1322
- let keyBytes;
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
- const { wrapped, key } = await seal(recipient, aesKey, opts);
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(wrapped.length, WRAPPED_LEN_PREFIX), wrapped, iv, ct);
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) {