nervur 0.12.0 → 0.14.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/GETTING-STARTED.md +111 -0
- package/README.md +44 -18
- package/SECURITY.md +11 -0
- package/being.mjs +127 -42
- package/blueprint.mjs +65 -15
- package/client.mjs +9 -6
- package/contract.mjs +6 -4
- package/ground.mjs +96 -36
- package/index.mjs +2 -2
- package/modules.mjs +13 -70
- package/package.json +5 -2
- package/program.mjs +1 -0
- package/tools.mjs +67 -48
- package/voice.mjs +49 -63
package/tools.mjs
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
createDecipheriv,
|
|
4
|
-
createHash,
|
|
5
|
-
createPublicKey,
|
|
6
|
-
randomBytes,
|
|
7
|
-
verify,
|
|
8
|
-
} from 'node:crypto';
|
|
1
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
9
3
|
|
|
10
|
-
const
|
|
4
|
+
const HEX = '0123456789abcdef';
|
|
5
|
+
|
|
6
|
+
const utf8 = new TextEncoder();
|
|
7
|
+
const text8 = new TextDecoder();
|
|
8
|
+
|
|
9
|
+
// Buffer is Node's alone. Every byte string here is a Uint8Array, and these
|
|
10
|
+
// are the encodings the substrate names: hex on a digest, base64 on a
|
|
11
|
+
// signature, utf8 under canon.
|
|
12
|
+
const Bytes = Object.freeze({
|
|
13
|
+
base64: (bytes) => {
|
|
14
|
+
let binary = '';
|
|
15
|
+
for (const byte of bytes) {
|
|
16
|
+
binary += String.fromCharCode(byte);
|
|
17
|
+
}
|
|
18
|
+
return btoa(binary);
|
|
19
|
+
},
|
|
20
|
+
concat: (parts) => {
|
|
21
|
+
const whole = new Uint8Array(parts.reduce((run, one) => run + one.length, 0));
|
|
22
|
+
let at = 0;
|
|
23
|
+
for (const one of parts) {
|
|
24
|
+
whole.set(one, at);
|
|
25
|
+
at += one.length;
|
|
26
|
+
}
|
|
27
|
+
return whole;
|
|
28
|
+
},
|
|
29
|
+
equal: (one, other) => one.length === other.length && one.every((byte, at) => byte === other[at]),
|
|
30
|
+
hex: (bytes) => {
|
|
31
|
+
let out = '';
|
|
32
|
+
for (const byte of bytes) {
|
|
33
|
+
out += HEX[byte >> 4] + HEX[byte & 15];
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
},
|
|
37
|
+
of: (value) => utf8.encode(value),
|
|
38
|
+
read: (bytes) => text8.decode(bytes),
|
|
39
|
+
unbase64: (value) => {
|
|
40
|
+
const binary = atob(value);
|
|
41
|
+
const bytes = new Uint8Array(binary.length);
|
|
42
|
+
for (let at = 0; at < binary.length; at += 1) {
|
|
43
|
+
bytes[at] = binary.charCodeAt(at);
|
|
44
|
+
}
|
|
45
|
+
return bytes;
|
|
46
|
+
},
|
|
47
|
+
unhex: (value) => {
|
|
48
|
+
const bytes = new Uint8Array(value.length >> 1);
|
|
49
|
+
for (let at = 0; at < bytes.length; at += 1) {
|
|
50
|
+
bytes[at] = Number.parseInt(value.substr(at << 1, 2), 16);
|
|
51
|
+
}
|
|
52
|
+
return bytes;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
11
55
|
|
|
12
56
|
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
13
57
|
|
|
@@ -70,21 +114,23 @@ const unbase32 = (text) => {
|
|
|
70
114
|
bits -= 8;
|
|
71
115
|
}
|
|
72
116
|
}
|
|
73
|
-
return
|
|
117
|
+
return Uint8Array.from(out);
|
|
74
118
|
};
|
|
75
119
|
|
|
76
120
|
const Tools = Object.freeze({
|
|
77
121
|
address: (pk) => {
|
|
78
|
-
const payload =
|
|
122
|
+
const payload = Bytes.concat([Uint8Array.of(VERSION_G), pk]);
|
|
79
123
|
const crc = crc16(payload);
|
|
80
|
-
return base32(
|
|
124
|
+
return base32(Bytes.concat([payload, Uint8Array.of(crc & 0xff, crc >> 8)]));
|
|
81
125
|
},
|
|
82
|
-
canon: (claim) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
126
|
+
canon: (claim) => Bytes.of(JSON.stringify(ordered(claim))),
|
|
127
|
+
// The label arrives as text and the digest is over its utf8 bytes, which is
|
|
128
|
+
// the byte string the substrate has always hashed.
|
|
129
|
+
derive: (root, label) => sha256.create().update(root).update(Bytes.of(label)).digest(),
|
|
130
|
+
digest: (bytes) => Bytes.hex(sha256(bytes)),
|
|
131
|
+
envelope: async (voice, to, op, seq) => {
|
|
86
132
|
const from = Tools.address(voice.pk);
|
|
87
|
-
return { from, op, seq, sig: voice.sign(Tools.canon({ from, op, seq, to })), to };
|
|
133
|
+
return { from, op, seq, sig: await voice.sign(Tools.canon({ from, op, seq, to })), to };
|
|
88
134
|
},
|
|
89
135
|
pkOf: (address) => {
|
|
90
136
|
const bytes = typeof address === 'string' && address.length === 56 ? unbase32(address) : null;
|
|
@@ -93,39 +139,12 @@ const Tools = Object.freeze({
|
|
|
93
139
|
}
|
|
94
140
|
const payload = bytes.subarray(0, 33);
|
|
95
141
|
const checksum = bytes[33] | (bytes[34] << 8);
|
|
96
|
-
return crc16(payload) === checksum ?
|
|
97
|
-
},
|
|
98
|
-
seal: (secret, value) => {
|
|
99
|
-
const iv = randomBytes(12);
|
|
100
|
-
const cipher = createCipheriv('aes-256-gcm', secret, iv);
|
|
101
|
-
const sealed = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
|
|
102
|
-
return Buffer.concat([iv, cipher.getAuthTag(), sealed]).toString('base64');
|
|
103
|
-
},
|
|
104
|
-
secret: () => randomBytes(32),
|
|
105
|
-
unseal: (secret, sealed) => {
|
|
106
|
-
try {
|
|
107
|
-
const bytes = Buffer.from(sealed, 'base64');
|
|
108
|
-
const decipher = createDecipheriv('aes-256-gcm', secret, bytes.subarray(0, 12));
|
|
109
|
-
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
110
|
-
return JSON.parse(
|
|
111
|
-
Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString('utf8'),
|
|
112
|
-
);
|
|
113
|
-
} catch {
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
142
|
+
return crc16(payload) === checksum ? payload.slice(1) : null;
|
|
116
143
|
},
|
|
144
|
+
secret: () => crypto.getRandomValues(new Uint8Array(32)),
|
|
117
145
|
verifies: (pk, bytes, sig) => {
|
|
118
146
|
try {
|
|
119
|
-
return verify(
|
|
120
|
-
null,
|
|
121
|
-
bytes,
|
|
122
|
-
createPublicKey({
|
|
123
|
-
key: Buffer.concat([ED25519_SPKI_PREFIX, pk]),
|
|
124
|
-
format: 'der',
|
|
125
|
-
type: 'spki',
|
|
126
|
-
}),
|
|
127
|
-
Buffer.from(sig, 'base64'),
|
|
128
|
-
);
|
|
147
|
+
return ed25519.verify(Bytes.unbase64(sig), bytes, pk);
|
|
129
148
|
} catch {
|
|
130
149
|
return false;
|
|
131
150
|
}
|
|
@@ -140,4 +159,4 @@ const Stamp = () => {
|
|
|
140
159
|
};
|
|
141
160
|
};
|
|
142
161
|
|
|
143
|
-
export { MEASURE, SPAN, Stamp, Tools };
|
|
162
|
+
export { Bytes, MEASURE, SPAN, Stamp, Tools };
|
package/voice.mjs
CHANGED
|
@@ -1,69 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
14
|
-
privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64');
|
|
12
|
+
const seeded = () => crypto.getRandomValues(new Uint8Array(32));
|
|
15
13
|
|
|
16
|
-
const
|
|
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
|
|
20
|
-
|
|
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 = (
|
|
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:
|
|
41
|
-
nextPkHash: next === undefined ? nextPkHash : Tools.digest(
|
|
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
|
|
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(
|
|
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
|
-
: {
|
|
59
|
-
|
|
60
|
-
|
|
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:
|
|
66
|
-
next:
|
|
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 =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
),
|