pinomad 0.3.0 → 0.5.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.
@@ -1,347 +0,0 @@
1
- // packages/protocol/src/relay.ts
2
- import { ed25519 } from "@noble/curves/ed25519.js";
3
-
4
- // packages/protocol/src/noise.ts
5
- import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
6
- import { x25519 } from "@noble/curves/ed25519.js";
7
- import { blake2s } from "@noble/hashes/blake2.js";
8
- import { hmac } from "@noble/hashes/hmac.js";
9
- var PROTOCOL_NAME = new TextEncoder().encode("Noise_IK_25519_ChaChaPoly_BLAKE2s");
10
- var KEY_LENGTH = 32;
11
- var TAG_LENGTH = 16;
12
- var MAX_MESSAGE = 65535;
13
- var MAX_NONCE = 2n ** 64n - 1n;
14
- var hash = (data) => blake2s(data, { dkLen: KEY_LENGTH });
15
- var concat = (...parts) => {
16
- const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
17
- let offset = 0;
18
- for (const part of parts) {
19
- out.set(part, offset);
20
- offset += part.length;
21
- }
22
- return out;
23
- };
24
- function hkdf(chainingKey, ikm, outputs) {
25
- const tempKey = hmac(blake2s, chainingKey, ikm);
26
- const out1 = hmac(blake2s, tempKey, new Uint8Array([1]));
27
- const out2 = hmac(blake2s, tempKey, concat(out1, new Uint8Array([2])));
28
- return [out1, out2];
29
- }
30
- function generateKeyPair() {
31
- return keyPairFromPrivate(x25519.utils.randomSecretKey());
32
- }
33
- function keyPairFromPrivate(privateKey) {
34
- if (privateKey.length !== KEY_LENGTH)
35
- throw new Error(`X25519 private key must be ${KEY_LENGTH} bytes`);
36
- return { publicKey: x25519.getPublicKey(privateKey), privateKey };
37
- }
38
- var dh = (pair, remotePublic) => x25519.getSharedSecret(pair.privateKey, remotePublic);
39
-
40
- class NoiseCipherState {
41
- #key;
42
- #nonce = 0n;
43
- setKey(key) {
44
- this.#key = key;
45
- this.#nonce = 0n;
46
- }
47
- encrypt(plaintext, ad = new Uint8Array(0)) {
48
- const key = this.#key;
49
- if (key === undefined)
50
- return plaintext;
51
- return chacha20poly1305(key, this.#takeNonce(), ad).encrypt(plaintext);
52
- }
53
- decrypt(ciphertext, ad = new Uint8Array(0)) {
54
- const key = this.#key;
55
- if (key === undefined)
56
- return ciphertext;
57
- return chacha20poly1305(key, this.#takeNonce(), ad).decrypt(ciphertext);
58
- }
59
- #takeNonce() {
60
- if (this.#nonce === MAX_NONCE)
61
- throw new Error("nonce exhausted");
62
- const nonce = new Uint8Array(12);
63
- new DataView(nonce.buffer).setBigUint64(4, this.#nonce, true);
64
- this.#nonce += 1n;
65
- return nonce;
66
- }
67
- }
68
-
69
- class SymmetricState {
70
- #ck;
71
- #h;
72
- cipher = new NoiseCipherState;
73
- constructor() {
74
- const name = PROTOCOL_NAME;
75
- this.#h = name.length <= KEY_LENGTH ? concat(name, new Uint8Array(KEY_LENGTH - name.length)) : hash(name);
76
- this.#ck = this.#h.slice();
77
- }
78
- mixKey(ikm) {
79
- const [ck, tempKey] = hkdf(this.#ck, ikm, 2);
80
- this.#ck = ck;
81
- this.cipher.setKey(tempKey.subarray(0, KEY_LENGTH));
82
- }
83
- mixHash(data) {
84
- this.#h = hash(concat(this.#h, data));
85
- }
86
- encryptAndHash(plaintext) {
87
- const ciphertext = this.cipher.encrypt(plaintext, this.#h);
88
- this.mixHash(ciphertext);
89
- return ciphertext;
90
- }
91
- decryptAndHash(ciphertext) {
92
- const plaintext = this.cipher.decrypt(ciphertext, this.#h);
93
- this.mixHash(ciphertext);
94
- return plaintext;
95
- }
96
- get handshakeHash() {
97
- return this.#h;
98
- }
99
- split() {
100
- const [key1, key2] = hkdf(this.#ck, new Uint8Array(0), 2);
101
- const first = new NoiseCipherState;
102
- first.setKey(key1.subarray(0, KEY_LENGTH));
103
- const second = new NoiseCipherState;
104
- second.setKey(key2.subarray(0, KEY_LENGTH));
105
- return [first, second];
106
- }
107
- }
108
- function take(message, size) {
109
- if (message.offset + size > message.buffer.length)
110
- throw new Error("malformed Noise message");
111
- const slice = message.buffer.subarray(message.offset, message.offset + size);
112
- message.offset += size;
113
- return slice;
114
- }
115
- function readMessage1(state, message, own) {
116
- const remoteEphemeral = take(message, KEY_LENGTH).slice();
117
- state.mixHash(remoteEphemeral);
118
- state.mixKey(dh(own.static, remoteEphemeral));
119
- const remoteStatic = state.decryptAndHash(take(message, KEY_LENGTH + TAG_LENGTH)).slice();
120
- if (remoteStatic.length !== KEY_LENGTH)
121
- throw new Error("bad remote static key");
122
- state.mixKey(dh(own.static, remoteStatic));
123
- return { remoteEphemeral, remoteStatic };
124
- }
125
- function writeMessage2(state, buffer, own, remoteEphemeral, remoteStatic) {
126
- buffer.push(own.ephemeral.publicKey);
127
- state.mixHash(own.ephemeral.publicKey);
128
- state.mixKey(dh(own.ephemeral, remoteEphemeral));
129
- state.mixKey(dh(own.ephemeral, remoteStatic));
130
- }
131
- var checkLength = (message, what) => {
132
- if (message.length > MAX_MESSAGE)
133
- throw new Error(`${what} exceeds ${MAX_MESSAGE} bytes`);
134
- };
135
- function initialize(prologue, responderStatic) {
136
- const state = new SymmetricState;
137
- state.mixHash(prologue);
138
- state.mixHash(responderStatic);
139
- return state;
140
- }
141
- function respondIK(options) {
142
- const ephemeral = options.ephemeral ?? generateKeyPair();
143
- const state = initialize(options.prologue, options.static.publicKey);
144
- let remoteEphemeral;
145
- let remoteStatic;
146
- return {
147
- readMessage1(message) {
148
- checkLength(message, "handshake message");
149
- const reader = { buffer: message, offset: 0 };
150
- const remote = readMessage1(state, reader, { static: options.static });
151
- remoteEphemeral = remote.remoteEphemeral;
152
- remoteStatic = remote.remoteStatic;
153
- const payload = state.decryptAndHash(take(reader, message.length - reader.offset));
154
- return { payload, remoteStatic };
155
- },
156
- writeMessage2(payload) {
157
- if (remoteEphemeral === undefined || remoteStatic === undefined)
158
- throw new Error("message 2 before message 1");
159
- const buffer = [];
160
- writeMessage2(state, buffer, { ephemeral }, remoteEphemeral, remoteStatic);
161
- buffer.push(state.encryptAndHash(payload));
162
- const message = concat(...buffer);
163
- checkLength(message, "handshake message");
164
- const [receive, send] = state.split();
165
- return { message, result: { send, receive, handshakeHash: state.handshakeHash } };
166
- }
167
- };
168
- }
169
-
170
- // packages/protocol/src/secure-channel.ts
171
- var SECURE_PROLOGUE = new TextEncoder().encode("pinomad/1");
172
- var utf8 = new TextEncoder;
173
- var utf8Decode = new TextDecoder("utf-8", { fatal: true });
174
- var encodeHostHello = (hello) => utf8.encode(JSON.stringify(hello));
175
- function decodeJson(bytes) {
176
- try {
177
- return JSON.parse(utf8Decode.decode(bytes));
178
- } catch {
179
- throw new Error("handshake payload is not JSON");
180
- }
181
- }
182
- var isString = (value) => typeof value === "string";
183
- function decodeClientHello(bytes) {
184
- const value = decodeJson(bytes);
185
- if (value?.v !== 1)
186
- throw new Error("unsupported client hello");
187
- const pair = value.pair;
188
- if (pair === undefined)
189
- return { v: 1 };
190
- const secret = pair.secret;
191
- const name = pair.name;
192
- if (!isString(secret) || !isString(name))
193
- throw new Error("malformed pair");
194
- return { v: 1, pair: { secret, name } };
195
- }
196
- var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
197
- function toBase64Url(bytes) {
198
- let out = "";
199
- for (let index = 0;index < bytes.length; index += 3) {
200
- const a = bytes[index];
201
- const b = bytes[index + 1];
202
- const c = bytes[index + 2];
203
- out += B64_ALPHABET[a >> 2] + B64_ALPHABET[(a & 3) << 4 | (b ?? 0) >> 4];
204
- if (b !== undefined)
205
- out += B64_ALPHABET[(b & 15) << 2 | (c ?? 0) >> 6];
206
- if (c !== undefined)
207
- out += B64_ALPHABET[c & 63];
208
- }
209
- return out;
210
- }
211
- function fromBase64Url(text) {
212
- if (text.length % 4 === 1)
213
- throw new Error("invalid base64url");
214
- const lookup = new Map(Array.from(B64_ALPHABET, (char, index) => [char, index]));
215
- const bytes = [];
216
- for (let index = 0;index < text.length; index += 4) {
217
- const quad = text.slice(index, index + 4);
218
- const values = [...quad].map((char) => {
219
- const value = lookup.get(char);
220
- if (value === undefined)
221
- throw new Error("invalid base64url");
222
- return value;
223
- });
224
- const packed = values.reduce((bits, value, index2) => bits | value << 18 - index2 * 6, 0);
225
- bytes.push(packed >> 16 & 255);
226
- if (values.length > 2)
227
- bytes.push(packed >> 8 & 255);
228
- if (values.length > 3)
229
- bytes.push(packed & 255);
230
- }
231
- return new Uint8Array(bytes);
232
- }
233
- var MAX_BODY = 65535 - 16 - 1;
234
-
235
- class SecureSession {
236
- #send;
237
- #receive;
238
- #pending = [];
239
- constructor(result) {
240
- this.#send = result.send;
241
- this.#receive = result.receive;
242
- }
243
- seal(text) {
244
- const body = utf8.encode(text);
245
- const chunks = [];
246
- for (let offset = 0;offset <= body.length; offset += MAX_BODY) {
247
- const last = offset + MAX_BODY >= body.length;
248
- const record = new Uint8Array(1 + Math.min(MAX_BODY, body.length - offset));
249
- record[0] = last ? 0 : 1;
250
- record.set(body.subarray(offset, offset + MAX_BODY), 1);
251
- chunks.push(this.#send.encrypt(record));
252
- if (last)
253
- break;
254
- }
255
- return chunks;
256
- }
257
- open(chunk) {
258
- const record = this.#receive.decrypt(chunk);
259
- const flag = record[0];
260
- if (flag !== 0 && flag !== 1)
261
- throw new Error("bad record flag");
262
- this.#pending.push(record.subarray(1));
263
- if (flag === 1)
264
- return;
265
- const whole = concatParts(this.#pending);
266
- this.#pending = [];
267
- return utf8Decode.decode(whole);
268
- }
269
- }
270
- function concatParts(parts) {
271
- const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
272
- let offset = 0;
273
- for (const part of parts) {
274
- out.set(part, offset);
275
- offset += part.length;
276
- }
277
- return out;
278
- }
279
-
280
- // packages/protocol/src/relay.ts
281
- var RELAY_CLOSE = {
282
- badSignature: 4601,
283
- hostNotAllowed: 4603,
284
- hostOffline: 4604,
285
- unknownConnection: 4605,
286
- replaced: 4609,
287
- acceptTimeout: 4610
288
- };
289
- function encodeRelayFrame(frame) {
290
- return JSON.stringify(frame);
291
- }
292
- var isString2 = (value) => typeof value === "string";
293
- function decodeJson2(text) {
294
- try {
295
- return JSON.parse(text);
296
- } catch {
297
- throw new Error("relay frame is not JSON");
298
- }
299
- }
300
- function decodeRelayToHost(text) {
301
- const value = decodeJson2(text);
302
- switch (value?.t) {
303
- case "challenge":
304
- if (!isString2(value.nonce))
305
- throw new Error("malformed challenge");
306
- return { t: "challenge", nonce: value.nonce };
307
- case "registered":
308
- return { t: "registered" };
309
- case "incoming":
310
- if (!isString2(value.connId))
311
- throw new Error("malformed incoming");
312
- return { t: "incoming", connId: value.connId };
313
- default:
314
- throw new Error("unknown relay frame");
315
- }
316
- }
317
- function decodeHostToRelay(text) {
318
- const value = decodeJson2(text);
319
- if (value?.t !== "register" || !isString2(value.hostId) || !isString2(value.signature)) {
320
- throw new Error("malformed register");
321
- }
322
- return { t: "register", hostId: value.hostId, signature: value.signature };
323
- }
324
- function relayHostId(publicKey) {
325
- if (publicKey.length !== 32)
326
- throw new Error("Ed25519 public key must be 32 bytes");
327
- return toBase64Url(publicKey);
328
- }
329
- var utf82 = new TextEncoder;
330
- function relayChallengeMessage(args) {
331
- return utf82.encode(`pinomad-relay/1
332
- ${args.relayOrigin}
333
- ${args.hostId}
334
- ${args.nonce}`);
335
- }
336
- function signRelayChallenge(secretKey, args) {
337
- return toBase64Url(ed25519.sign(relayChallengeMessage(args), secretKey));
338
- }
339
- function verifyRelayChallenge(signature, args) {
340
- try {
341
- return ed25519.verify(fromBase64Url(signature), relayChallengeMessage(args), fromBase64Url(args.hostId));
342
- } catch {
343
- return false;
344
- }
345
- }
346
-
347
- export { generateKeyPair, keyPairFromPrivate, respondIK, SECURE_PROLOGUE, encodeHostHello, decodeClientHello, toBase64Url, fromBase64Url, SecureSession, RELAY_CLOSE, encodeRelayFrame, decodeRelayToHost, decodeHostToRelay, relayHostId, signRelayChallenge, verifyRelayChallenge };