nervur 0.12.0 → 0.13.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/voice.mjs CHANGED
@@ -1,69 +1,63 @@
1
- import {
2
- createHash,
3
- createPrivateKey,
4
- createPublicKey,
5
- diffieHellman,
6
- generateKeyPairSync,
7
- sign,
8
- } from 'node:crypto';
9
- import { Tools } from './tools.mjs';
1
+ import { ed25519, x25519 } from '@noble/curves/ed25519.js';
2
+ import { sha256 } from '@noble/hashes/sha2.js';
3
+ import { Bytes, Tools } from './tools.mjs';
10
4
 
11
- const X25519_SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
5
+ // A private key travels as PKCS8 DER in base64 — the shape every stored
6
+ // bundle and every hand on the wire already carries — and a PKCS8 Ed25519 or
7
+ // X25519 key is one fixed prefix and the thirty-two seed bytes, so the bytes
8
+ // are the same whoever assembles them.
9
+ const ED_PKCS8 = Bytes.unhex('302e020100300506032b657004220420');
10
+ const X_PKCS8 = Bytes.unhex('302e020100300506032b656e04220420');
12
11
 
13
- const packed = (privateKey) =>
14
- privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64');
12
+ const seeded = () => crypto.getRandomValues(new Uint8Array(32));
15
13
 
16
- const unpacked = (text) =>
17
- createPrivateKey({ key: Buffer.from(text, 'base64'), format: 'der', type: 'pkcs8' });
14
+ const packed = (raw, prefix) => Bytes.base64(Bytes.concat([prefix, raw]));
18
15
 
19
- const rawPk = (privateKey) =>
20
- createPublicKey(privateKey).export({ format: 'der', type: 'spki' }).subarray(-32);
16
+ const unpacked = (text, prefix) => {
17
+ const der = Bytes.unbase64(text);
18
+ if (der.length !== prefix.length + 32 || !Bytes.equal(der.subarray(0, prefix.length), prefix)) {
19
+ throw new Error('not a key of this curve');
20
+ }
21
+ return der.subarray(prefix.length);
22
+ };
21
23
 
22
- const agreed = (privateKey, rawPub) =>
23
- createHash('sha256')
24
- .update(
25
- diffieHellman({
26
- privateKey,
27
- publicKey: createPublicKey({
28
- key: Buffer.concat([X25519_SPKI_PREFIX, rawPub]),
29
- format: 'der',
30
- type: 'spki',
31
- }),
32
- }),
33
- )
34
- .digest();
24
+ const agreed = (box, rawPub) => sha256(x25519.getSharedSecret(box, rawPub));
35
25
 
36
26
  const committed = /^[0-9a-f]{64}$/;
37
27
 
38
28
  const voiced = ({ present, next, box, nextPkHash }) =>
39
29
  Object.freeze({
40
- boxPk: rawPk(box),
41
- nextPkHash: next === undefined ? nextPkHash : Tools.digest(rawPk(next)),
42
- open: (wrapped) => {
30
+ boxPk: x25519.getPublicKey(box),
31
+ nextPkHash: next === undefined ? nextPkHash : Tools.digest(ed25519.getPublicKey(next)),
32
+ open: (wrapped, cipher) => {
43
33
  try {
44
34
  const [ephemeral, sealed] = wrapped.split('.');
45
- return Tools.unseal(agreed(box, Buffer.from(ephemeral, 'base64')), sealed);
35
+ return cipher.unseal(agreed(box, Bytes.unbase64(ephemeral)), sealed);
46
36
  } catch {
47
37
  return undefined;
48
38
  }
49
39
  },
50
40
  hand: () => ({
51
- box: packed(box),
52
- nextPkHash: next === undefined ? nextPkHash : Tools.digest(rawPk(next)),
53
- present: packed(present),
41
+ box: packed(box, X_PKCS8),
42
+ nextPkHash: next === undefined ? nextPkHash : Tools.digest(ed25519.getPublicKey(next)),
43
+ present: packed(present, ED_PKCS8),
54
44
  }),
55
45
  pack: () =>
56
46
  next === undefined
57
- ? { box: packed(box), nextPkHash, present: packed(present) }
58
- : { box: packed(box), next: packed(next), present: packed(present) },
59
- pk: rawPk(present),
60
- sign: (bytes) => sign(null, bytes, present).toString('base64'),
47
+ ? { box: packed(box, X_PKCS8), nextPkHash, present: packed(present, ED_PKCS8) }
48
+ : {
49
+ box: packed(box, X_PKCS8),
50
+ next: packed(next, ED_PKCS8),
51
+ present: packed(present, ED_PKCS8),
52
+ },
53
+ pk: ed25519.getPublicKey(present),
54
+ sign: (bytes) => Bytes.base64(ed25519.sign(bytes, present)),
61
55
  succeed: () =>
62
56
  next === undefined
63
57
  ? undefined
64
58
  : voiced({
65
- box: generateKeyPairSync('x25519').privateKey,
66
- next: generateKeyPairSync('ed25519').privateKey,
59
+ box: seeded(),
60
+ next: seeded(),
67
61
  present: next,
68
62
  }),
69
63
  });
@@ -72,18 +66,10 @@ const Voice = Object.freeze(
72
66
  Object.assign(
73
67
  (nextPkHash) => {
74
68
  if (nextPkHash === undefined) {
75
- return voiced({
76
- box: generateKeyPairSync('x25519').privateKey,
77
- next: generateKeyPairSync('ed25519').privateKey,
78
- present: generateKeyPairSync('ed25519').privateKey,
79
- });
69
+ return voiced({ box: seeded(), next: seeded(), present: seeded() });
80
70
  }
81
71
  return committed.test(nextPkHash)
82
- ? voiced({
83
- box: generateKeyPairSync('x25519').privateKey,
84
- nextPkHash,
85
- present: generateKeyPairSync('ed25519').privateKey,
86
- })
72
+ ? voiced({ box: seeded(), nextPkHash, present: seeded() })
87
73
  : undefined;
88
74
  },
89
75
  {
@@ -92,27 +78,27 @@ const Voice = Object.freeze(
92
78
  if (bundle.next === undefined) {
93
79
  return committed.test(bundle.nextPkHash)
94
80
  ? voiced({
95
- box: unpacked(bundle.box),
81
+ box: unpacked(bundle.box, X_PKCS8),
96
82
  nextPkHash: bundle.nextPkHash,
97
- present: unpacked(bundle.present),
83
+ present: unpacked(bundle.present, ED_PKCS8),
98
84
  })
99
85
  : undefined;
100
86
  }
101
87
  return voiced({
102
- box: unpacked(bundle.box),
103
- next: unpacked(bundle.next),
104
- present: unpacked(bundle.present),
88
+ box: unpacked(bundle.box, X_PKCS8),
89
+ next: unpacked(bundle.next, ED_PKCS8),
90
+ present: unpacked(bundle.present, ED_PKCS8),
105
91
  });
106
92
  } catch {
107
93
  return undefined;
108
94
  }
109
95
  },
110
- sealTo: (boxPk, value) => {
111
- const ephemeral = generateKeyPairSync('x25519');
112
- return `${rawPk(ephemeral.privateKey).toString('base64')}.${Tools.seal(
113
- agreed(ephemeral.privateKey, boxPk),
114
- value,
115
- )}`;
96
+ sealTo: (boxPk, value, cipher) => {
97
+ const ephemeral = seeded();
98
+ const shut = cipher?.seal(agreed(ephemeral, boxPk), value);
99
+ return shut === undefined
100
+ ? undefined
101
+ : `${Bytes.base64(x25519.getPublicKey(ephemeral))}.${shut}`;
116
102
  },
117
103
  },
118
104
  ),