simple-hpke 0.0.11 → 0.1.0

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,11 @@ 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
80
 
81
81
  // Later, recover the same key with your private key.
82
- const recoveredKey = await open(keypair, wrapped)
82
+ const recoveredKey = await open(keypair, sealedKey)
83
83
 
84
84
  // `recoveredKey` is equal to `aesKey`
85
85
  ```
@@ -98,7 +98,7 @@ along with the IV. The recipient uses their private key to open the AES key and
98
98
  decrypt the message.
99
99
 
100
100
  ```ts
101
- import { seal, open } from 'simple-hpke'
101
+ import { create, open } from 'simple-hpke'
102
102
 
103
103
  const recipient = await crypto.subtle.generateKey(
104
104
  { name: 'X25519' },
@@ -106,31 +106,31 @@ const recipient = await crypto.subtle.generateKey(
106
106
  ['deriveBits']
107
107
  )
108
108
 
109
- // Create and seal a fresh AES-GCM key, and encrypt a message with it.
110
- const { wrapped, key } = await seal(recipient)
109
+ // Create a fresh AES-GCM key, and encrypt a message with it.
110
+ const { enc, key } = await create(recipient)
111
111
  const iv = crypto.getRandomValues(new Uint8Array(12))
112
- const ciphertext = await crypto.subtle.encrypt(
112
+ const ciphertextBuffer = await crypto.subtle.encrypt(
113
113
  { name: 'AES-GCM', iv },
114
114
  key,
115
115
  new TextEncoder().encode('attack at dawn')
116
116
  )
117
117
 
118
- // Send `wrapped`, `iv`, and `ciphertext` together. `wrapped` is a fixed
118
+ // Send `enc`, `iv`, and `ciphertext` together. `enc` is a fixed
119
119
  // 80 bytes for this suite, and the AES-GCM IV is 12 bytes, so the recipient
120
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)
121
+ const ciphertext = new Uint8Array(ciphertextBuffer)
122
+ const message = new Uint8Array(enc.length + iv.length + ciphertext.length)
123
+ message.set(enc, 0)
124
+ message.set(iv, enc.length)
125
+ message.set(ciphertext, enc.length + iv.length)
126
126
 
127
127
  // On the other side, split the payload back into its parts.
128
- const wrapped2 = message.subarray(0, 80)
128
+ const enc2 = message.subarray(0, 80)
129
129
  const iv2 = message.subarray(80, 80 + 12)
130
130
  const ciphertext2 = message.subarray(80 + 12)
131
131
 
132
132
  // Recover the key, then decrypt the message.
133
- const recovered = await open(recipient, wrapped2)
133
+ const recovered = await open(recipient, enc2)
134
134
  const plaintext = await crypto.subtle.decrypt(
135
135
  { name: 'AES-GCM', iv: iv2 },
136
136
  recovered,
@@ -146,7 +146,8 @@ So that was a lot of code to encrypt and decrypt a message...
146
146
  This package exposes functions `encrypt` and `decrypt` which do the same thing.
147
147
 
148
148
  `encrypt` seals an AES key to the recipient, encrypts the message under
149
- that key, and returns a single envelope: `wrappedLen + wrapped + iv + ciphertext`
149
+ that key, and returns a single envelope:
150
+ `wrappedLen + wrapped + iv + ciphertext`
150
151
  (a 2-byte length prefix, the wrapped key, the 12-byte AES-GCM IV, and the
151
152
  cipher text). `decrypt` reverses it, returning the plaintext bytes.
152
153
 
@@ -156,8 +157,8 @@ cipher text). `decrypt` reverses it, returning the plaintext bytes.
156
157
  > [`decrypt.fromString`](#decryptfromstring) below for a convenient way
157
158
  > to decrypt from a string.
158
159
  >
159
- > See [`encrypt.asString`](#encrypt-to-a-string) for encrypting and ecoding to
160
- > a string.
160
+ > See [`encrypt.asString`](#encryptasstring) for encrypting and encoding to a
161
+ > string.
161
162
  >
162
163
 
163
164
 
@@ -249,7 +250,7 @@ bytes to `decrypt` / `decrypt.asString`.
249
250
 
250
251
  ##### `decrypt`
251
252
 
252
- Descrypt the given data, return a `Uint8Array`.
253
+ Decrypt the given data, return a `Uint8Array`.
253
254
 
254
255
  ```ts
255
256
  async function decrypt (
@@ -261,7 +262,7 @@ async function decrypt (
261
262
 
262
263
  ##### `decrypt.asString`
263
264
 
264
- Take a `Uint8Array`, return a sttring.
265
+ Take a `Uint8Array`, return a string.
265
266
 
266
267
  ```ts
267
268
  decrypt.asString = async function decryptToString (
@@ -293,7 +294,7 @@ This exposes ESM and common JS via
293
294
 
294
295
  ### ESM
295
296
  ```js
296
- import { seal, open, encrypt, decrypt } from 'simple-hpke'
297
+ import { create, seal, open, encrypt, decrypt } from 'simple-hpke'
297
298
  ```
298
299
 
299
300
  ### 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) {