salty-crypto 0.0.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 +63 -0
- package/dist/salty-crypto.js +1 -0
- package/package.json +39 -0
- package/rollup.config.js +9 -0
- package/src/aead.ts +86 -0
- package/src/blake2.ts +135 -0
- package/src/chacha20.ts +72 -0
- package/src/index.ts +12 -0
- package/src/noise.ts +368 -0
- package/src/patterns.ts +59 -0
- package/src/poly1305.ts +369 -0
- package/src/profiles.ts +65 -0
- package/src/random.ts +36 -0
- package/src/x25519.ts +596 -0
- package/test/harness.ts +73 -0
- package/test/tests/aead.test.ts +49 -0
- package/test/tests/blake2.test.ts +41 -0
- package/test/tests/chacha20.test.ts +81 -0
- package/test/tests/noise.test.ts +144 -0
- package/test/tests/poly1305.test.ts +16 -0
- package/test-vectors/noise-c-basic.txt +19684 -0
- package/test-vectors/snow.txt +10348 -0
- package/tsconfig.json +18 -0
package/src/noise.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
export type DHKeyPair = { public: Uint8Array, secret: Uint8Array };
|
|
5
|
+
|
|
6
|
+
export class Nonce {
|
|
7
|
+
constructor(public lo = 0, public hi = 0) {}
|
|
8
|
+
|
|
9
|
+
increment() {
|
|
10
|
+
const oldLo = this.lo;
|
|
11
|
+
const newLo = (oldLo + 1) | 0;
|
|
12
|
+
this.lo = newLo;
|
|
13
|
+
if (newLo < oldLo) this.hi = (this.hi + 1) | 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
reset(lo = 0, hi = 0) {
|
|
17
|
+
this.lo = lo;
|
|
18
|
+
this.hi = hi;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static get MAX(): Nonce {
|
|
22
|
+
return new Nonce(0xffffffff, 0xffffffff);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function bytesXor(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
27
|
+
const len = Math.min(a.byteLength, b.byteLength);
|
|
28
|
+
const r = new Uint8Array(len);
|
|
29
|
+
for (let i = 0; i < len; i++) r[i] = a[i] ^ b[i];
|
|
30
|
+
return r;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function bytesAppend(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
34
|
+
const r = new Uint8Array(a.byteLength + b.byteLength);
|
|
35
|
+
r.set(a, 0);
|
|
36
|
+
r.set(b, a.byteLength);
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const EMPTY_BYTES = new Uint8Array(0);
|
|
41
|
+
|
|
42
|
+
export type HMAC = (key: Uint8Array, data: Uint8Array) => Uint8Array;
|
|
43
|
+
|
|
44
|
+
function makeHMAC(algorithms: NoiseProtocolAlgorithms): HMAC {
|
|
45
|
+
const HMAC_IPAD = new Uint8Array(algorithms.hashBlocklen()); HMAC_IPAD.fill(0x36);
|
|
46
|
+
const HMAC_OPAD = new Uint8Array(algorithms.hashBlocklen()); HMAC_OPAD.fill(0x5c);
|
|
47
|
+
return (key0, data) => {
|
|
48
|
+
const key = algorithms._padOrHash(key0, algorithms.hashBlocklen());
|
|
49
|
+
return algorithms.hash(bytesAppend(bytesXor(key, HMAC_OPAD),
|
|
50
|
+
algorithms.hash(bytesAppend(bytesXor(key, HMAC_IPAD),
|
|
51
|
+
data))));
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export abstract class NoiseProtocolAlgorithms {
|
|
56
|
+
readonly dhlen: number;
|
|
57
|
+
readonly hmac: HMAC;
|
|
58
|
+
|
|
59
|
+
constructor (hmac?: HMAC) {
|
|
60
|
+
const tmp = this.generateKeypair();
|
|
61
|
+
this.dhlen = this.dh(tmp, tmp.public).byteLength;
|
|
62
|
+
this.hmac = hmac ?? makeHMAC(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
abstract dhName(): string;
|
|
66
|
+
abstract generateKeypair(): DHKeyPair;
|
|
67
|
+
abstract dh(kp: DHKeyPair, pk: Uint8Array): Uint8Array;
|
|
68
|
+
|
|
69
|
+
abstract cipherName(): string;
|
|
70
|
+
abstract encrypt(key: DataView, nonce: Nonce, p: Uint8Array, associated_data?: Uint8Array): Uint8Array;
|
|
71
|
+
abstract decrypt(key: DataView, nonce: Nonce, c: Uint8Array, associated_data?: Uint8Array): Uint8Array;
|
|
72
|
+
|
|
73
|
+
abstract hashName(): string;
|
|
74
|
+
abstract hash(data: Uint8Array): Uint8Array;
|
|
75
|
+
abstract hashBlocklen(): number;
|
|
76
|
+
|
|
77
|
+
rekey(k: DataView): DataView {
|
|
78
|
+
return new DataView(this.encrypt(k, Nonce.MAX, new Uint8Array(32)).buffer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_padOrHash(bs0: Uint8Array, len: number): Uint8Array {
|
|
82
|
+
const bs = bs0.byteLength > len ? this.hash(bs0) : bs0;
|
|
83
|
+
return bytesAppend(bs, new Uint8Array(len - bs.byteLength));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
hkdf(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 2): [Uint8Array, Uint8Array];
|
|
87
|
+
hkdf(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 3): [Uint8Array, Uint8Array, Uint8Array];
|
|
88
|
+
hkdf(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 2 | 3): Uint8Array[] {
|
|
89
|
+
const tempKey = this.hmac(chainingKey, input);
|
|
90
|
+
const o1 = this.hmac(tempKey, Uint8Array.from([1]));
|
|
91
|
+
const o2 = this.hmac(tempKey, bytesAppend(o1, Uint8Array.from([2])));
|
|
92
|
+
switch (numOutputs) {
|
|
93
|
+
case 2: return [o1, o2];
|
|
94
|
+
case 3: return [o1, o2, this.hmac(tempKey, bytesAppend(o2, Uint8Array.from([3])))];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
matchingPattern(protocol_name: string): string | null {
|
|
99
|
+
const r = new RegExp(`^Noise_([A-Za-z0-9+]+)_${this.dhName()}_${this.cipherName()}_${this.hashName()}$`);
|
|
100
|
+
const m = r.exec(protocol_name);
|
|
101
|
+
if (m === null) return null;
|
|
102
|
+
return m[1];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface HandshakePattern {
|
|
107
|
+
name: string; // e.g. "NNpsk2"
|
|
108
|
+
baseName: string; // e.g. "NN"
|
|
109
|
+
messages: Token[][];
|
|
110
|
+
initiatorPreMessage: PreMessage;
|
|
111
|
+
responderPreMessage: PreMessage;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class CipherState {
|
|
115
|
+
view: DataView | null = null;
|
|
116
|
+
nonce = new Nonce();
|
|
117
|
+
|
|
118
|
+
constructor (public algorithms: NoiseProtocolAlgorithms,
|
|
119
|
+
key?: Uint8Array)
|
|
120
|
+
{
|
|
121
|
+
if (key !== void 0) this.view = new DataView(key.buffer);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
encrypt(plaintext: Uint8Array, associated_data?: Uint8Array): Uint8Array {
|
|
125
|
+
if (this.view === null) return plaintext;
|
|
126
|
+
const ciphertext = this.algorithms.encrypt(this.view, this.nonce, plaintext, associated_data);
|
|
127
|
+
this.nonce.increment();
|
|
128
|
+
return ciphertext;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
decrypt(ciphertext: Uint8Array, associated_data?: Uint8Array): Uint8Array {
|
|
132
|
+
if (this.view === null) return ciphertext;
|
|
133
|
+
const plaintext = this.algorithms.decrypt(this.view, this.nonce, ciphertext, associated_data);
|
|
134
|
+
this.nonce.increment();
|
|
135
|
+
return plaintext;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
rekey() {
|
|
139
|
+
if (this.view === null) return;
|
|
140
|
+
this.view = this.algorithms.rekey(this.view);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type Role = 'initiator' | 'responder';
|
|
145
|
+
|
|
146
|
+
export type NoiseProtocolOptions = {
|
|
147
|
+
prologue?: Uint8Array,
|
|
148
|
+
staticKeypair?: DHKeyPair,
|
|
149
|
+
remoteStaticPublicKey?: Uint8Array,
|
|
150
|
+
pregeneratedEphemeralKeypair?: DHKeyPair,
|
|
151
|
+
remotePregeneratedEphemeralPublicKey?: Uint8Array,
|
|
152
|
+
preSharedKeys?: Uint8Array[],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type KeyTransferToken = 'e' | 's';
|
|
156
|
+
export type KeyMixToken = 'ee' | 'es' | 'se' | 'ss' | 'psk';
|
|
157
|
+
export type Token = KeyTransferToken | KeyMixToken;
|
|
158
|
+
export type PreMessage = ['e'] | ['s'] | ['e', 's'] | [];
|
|
159
|
+
|
|
160
|
+
export type TransportState = { send: CipherState, recv: CipherState };
|
|
161
|
+
|
|
162
|
+
export class NoiseHandshake {
|
|
163
|
+
staticKeypair: DHKeyPair;
|
|
164
|
+
remoteStaticPublicKey: Uint8Array | null;
|
|
165
|
+
ephemeralKeypair: DHKeyPair;
|
|
166
|
+
remoteEphemeralPublicKey: Uint8Array | null;
|
|
167
|
+
preSharedKeys?: Uint8Array[];
|
|
168
|
+
stepIndex = 0;
|
|
169
|
+
cipherState: CipherState;
|
|
170
|
+
chainingKey: Uint8Array;
|
|
171
|
+
handshakeHash: Uint8Array;
|
|
172
|
+
|
|
173
|
+
constructor (public algorithms: NoiseProtocolAlgorithms,
|
|
174
|
+
public pattern: HandshakePattern,
|
|
175
|
+
public role: Role,
|
|
176
|
+
options: NoiseProtocolOptions = {})
|
|
177
|
+
{
|
|
178
|
+
this.staticKeypair = options.staticKeypair ?? this.algorithms.generateKeypair();
|
|
179
|
+
this.remoteStaticPublicKey = options.remoteStaticPublicKey ?? null;
|
|
180
|
+
this.ephemeralKeypair = options.pregeneratedEphemeralKeypair ?? this.algorithms.generateKeypair();
|
|
181
|
+
this.remoteEphemeralPublicKey = options.remotePregeneratedEphemeralPublicKey ?? null;
|
|
182
|
+
this.preSharedKeys = options.preSharedKeys;
|
|
183
|
+
if (this.preSharedKeys) {
|
|
184
|
+
this.preSharedKeys = this.preSharedKeys.slice();
|
|
185
|
+
if (this.preSharedKeys.length === 0) this.preSharedKeys = void 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const protocolName = new TextEncoder().encode(
|
|
189
|
+
'Noise_' + this.pattern.name +
|
|
190
|
+
'_' + this.algorithms.dhName() +
|
|
191
|
+
'_' + this.algorithms.cipherName() +
|
|
192
|
+
'_' + this.algorithms.hashName());
|
|
193
|
+
|
|
194
|
+
this.cipherState = new CipherState(this.algorithms);
|
|
195
|
+
this.chainingKey = this.algorithms._padOrHash(
|
|
196
|
+
protocolName,
|
|
197
|
+
this.algorithms.hash(EMPTY_BYTES).byteLength);
|
|
198
|
+
this.handshakeHash = this.chainingKey;
|
|
199
|
+
|
|
200
|
+
this.mixHash(options.prologue ?? EMPTY_BYTES);
|
|
201
|
+
this.pattern.initiatorPreMessage.forEach(t => this.mixHash(t === 'e'
|
|
202
|
+
? (this.isInitiator ? this.ephemeralKeypair.public : this.remoteEphemeralPublicKey!)
|
|
203
|
+
: (this.isInitiator ? this.staticKeypair.public : this.remoteStaticPublicKey!)));
|
|
204
|
+
this.pattern.responderPreMessage.forEach(t => this.mixHash(t === 'e'
|
|
205
|
+
? (!this.isInitiator ? this.ephemeralKeypair.public : this.remoteEphemeralPublicKey!)
|
|
206
|
+
: (!this.isInitiator ? this.staticKeypair.public : this.remoteStaticPublicKey!)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get isInitiator(): boolean {
|
|
210
|
+
return this.role === 'initiator';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mixHash(data: Uint8Array) {
|
|
214
|
+
this.handshakeHash = this.algorithms.hash(bytesAppend(this.handshakeHash, data));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
mixKey(input: Uint8Array) {
|
|
218
|
+
const [newCk, k] = this.algorithms.hkdf(this.chainingKey, input, 2);
|
|
219
|
+
this.chainingKey = newCk;
|
|
220
|
+
this.cipherState = new CipherState(this.algorithms, k);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
mixKeyAndHashNextPSK() {
|
|
224
|
+
const psk = this.preSharedKeys!.shift()!;
|
|
225
|
+
const [newCk, tempH, k] = this.algorithms.hkdf(this.chainingKey, psk, 3);
|
|
226
|
+
this.chainingKey = newCk;
|
|
227
|
+
this.mixHash(tempH);
|
|
228
|
+
this.cipherState = new CipherState(this.algorithms, k);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
encryptAndHash(p: Uint8Array) {
|
|
232
|
+
const c = this.cipherState.encrypt(p, this.handshakeHash);
|
|
233
|
+
this.mixHash(c);
|
|
234
|
+
return c;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
decryptAndHash(c: Uint8Array) {
|
|
238
|
+
const p = this.cipherState.decrypt(c, this.handshakeHash);
|
|
239
|
+
this.mixHash(c);
|
|
240
|
+
return p;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_split(): TransportState | null {
|
|
244
|
+
if (this.stepIndex < this.pattern.messages.length) {
|
|
245
|
+
return null;
|
|
246
|
+
} else {
|
|
247
|
+
let [kI, kR] = this.algorithms.hkdf(this.chainingKey, EMPTY_BYTES, 2)
|
|
248
|
+
.map(k => new CipherState(this.algorithms, k));
|
|
249
|
+
return this.isInitiator ? { send: kI, recv: kR } : { send: kR, recv: kI };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_nextStep(): Token[] {
|
|
254
|
+
if (this.stepIndex >= this.pattern.messages.length) {
|
|
255
|
+
throw new Error("Handshake already complete, cannot continue");
|
|
256
|
+
}
|
|
257
|
+
return this.pattern.messages[this.stepIndex++];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_processKeyMixToken(t: KeyMixToken) {
|
|
261
|
+
switch (t) {
|
|
262
|
+
case 'ee':
|
|
263
|
+
this.mixKey(this.algorithms.dh(this.ephemeralKeypair, this.remoteEphemeralPublicKey!));
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'es':
|
|
267
|
+
this.mixKey(this.isInitiator
|
|
268
|
+
? this.algorithms.dh(this.ephemeralKeypair, this.remoteStaticPublicKey!)
|
|
269
|
+
: this.algorithms.dh(this.staticKeypair, this.remoteEphemeralPublicKey!));
|
|
270
|
+
break;
|
|
271
|
+
|
|
272
|
+
case 'se':
|
|
273
|
+
this.mixKey(!this.isInitiator
|
|
274
|
+
? this.algorithms.dh(this.ephemeralKeypair, this.remoteStaticPublicKey!)
|
|
275
|
+
: this.algorithms.dh(this.staticKeypair, this.remoteEphemeralPublicKey!));
|
|
276
|
+
break;
|
|
277
|
+
|
|
278
|
+
case 'ss':
|
|
279
|
+
this.mixKey(this.algorithms.dh(this.staticKeypair, this.remoteStaticPublicKey!));
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case 'psk':
|
|
283
|
+
this.mixKeyAndHashNextPSK();
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
writeMessage(payload: Uint8Array): { packet: Uint8Array, finished: TransportState | null } {
|
|
289
|
+
const pieces = [];
|
|
290
|
+
this._nextStep().forEach(t => {
|
|
291
|
+
switch (t) {
|
|
292
|
+
case 'e':
|
|
293
|
+
pieces.push(this.ephemeralKeypair.public);
|
|
294
|
+
this.mixHash(this.ephemeralKeypair.public);
|
|
295
|
+
if (this.preSharedKeys) this.mixKey(this.ephemeralKeypair.public);
|
|
296
|
+
break;
|
|
297
|
+
|
|
298
|
+
case 's':
|
|
299
|
+
pieces.push(this.encryptAndHash(this.staticKeypair.public));
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
default:
|
|
303
|
+
this._processKeyMixToken(t);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
pieces.push(this.encryptAndHash(payload));
|
|
308
|
+
|
|
309
|
+
let packet: Uint8Array;
|
|
310
|
+
if (pieces.length === 1) {
|
|
311
|
+
packet = pieces[0];
|
|
312
|
+
} else {
|
|
313
|
+
packet = new Uint8Array(pieces.reduce((ac, p) => ac + p.byteLength, 0));
|
|
314
|
+
let offset = 0;
|
|
315
|
+
pieces.forEach(p => { packet.set(p, offset); offset += p.byteLength; });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { packet, finished: this._split() };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
readMessage(packet: Uint8Array): { message: Uint8Array, finished: TransportState | null } {
|
|
322
|
+
const take = (n: number): Uint8Array => {
|
|
323
|
+
const bs = packet.slice(0, n);
|
|
324
|
+
packet = packet.subarray(n);
|
|
325
|
+
return bs;
|
|
326
|
+
};
|
|
327
|
+
this._nextStep().forEach(t => {
|
|
328
|
+
switch (t) {
|
|
329
|
+
case 'e':
|
|
330
|
+
this.remoteEphemeralPublicKey = take(this.algorithms.dhlen);
|
|
331
|
+
this.mixHash(this.remoteEphemeralPublicKey);
|
|
332
|
+
if (this.preSharedKeys) this.mixKey(this.remoteEphemeralPublicKey);
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 's':
|
|
336
|
+
this.remoteStaticPublicKey = this.decryptAndHash(take(
|
|
337
|
+
this.algorithms.dhlen + (this.cipherState.view ? 16 : 0)));
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
default:
|
|
341
|
+
this._processKeyMixToken(t);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const message = this.decryptAndHash(packet);
|
|
347
|
+
return { message, finished: this._split() };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async completeHandshake(writePacket: (packet: Uint8Array) => Promise<void>,
|
|
351
|
+
readPacket: () => Promise<Uint8Array>,
|
|
352
|
+
handleMessage = async (_m: Uint8Array): Promise<void> => {},
|
|
353
|
+
produceMessage = async (): Promise<Uint8Array> => new Uint8Array(0))
|
|
354
|
+
: Promise<TransportState>
|
|
355
|
+
{
|
|
356
|
+
const W = async (): Promise<TransportState> => {
|
|
357
|
+
const { packet, finished } = this.writeMessage(await produceMessage());
|
|
358
|
+
await writePacket(packet);
|
|
359
|
+
return finished || R();
|
|
360
|
+
};
|
|
361
|
+
const R = async (): Promise<TransportState> => {
|
|
362
|
+
const { message, finished } = this.readMessage(await readPacket());
|
|
363
|
+
await handleMessage(message);
|
|
364
|
+
return finished || W();
|
|
365
|
+
};
|
|
366
|
+
return (this.isInitiator ? W() : R());
|
|
367
|
+
}
|
|
368
|
+
}
|
package/src/patterns.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import type { HandshakePattern, PreMessage, Token } from './noise';
|
|
5
|
+
|
|
6
|
+
export const PATTERNS: { [key: string]: HandshakePattern } = {};
|
|
7
|
+
|
|
8
|
+
function _p(
|
|
9
|
+
name: string,
|
|
10
|
+
messages: Token[][],
|
|
11
|
+
initiatorPreMessage: PreMessage,
|
|
12
|
+
responderPreMessage: PreMessage,
|
|
13
|
+
) {
|
|
14
|
+
const pat = { name, baseName: name, messages, initiatorPreMessage, responderPreMessage };
|
|
15
|
+
PATTERNS[pat.name] = pat;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_p("N", [["e", "es"]], [], ["s"]);
|
|
19
|
+
_p("K", [["e", "es", "ss"]], ["s"], ["s"]);
|
|
20
|
+
_p("X", [["e", "es", "s", "ss"]], [], ["s"]);
|
|
21
|
+
_p("NN", [["e"], ["e", "ee"]], [], []);
|
|
22
|
+
_p("NK", [["e", "es"], ["e", "ee"]], [], ["s"]);
|
|
23
|
+
_p("NX", [["e"], ["e", "ee", "s", "es"]], [], []);
|
|
24
|
+
_p("KN", [["e"], ["e", "ee", "se"]], ["s"], []);
|
|
25
|
+
_p("KK", [["e", "es", "ss"], ["e", "ee", "se"]], ["s"], ["s"]);
|
|
26
|
+
_p("KX", [["e"], ["e", "ee", "se", "s", "es"]], ["s"], []);
|
|
27
|
+
_p("XN", [["e"], ["e", "ee"], ["s", "se"]], [], []);
|
|
28
|
+
_p("XK", [["e", "es"], ["e", "ee"], ["s", "se"]], [], ["s"]);
|
|
29
|
+
_p("XX", [["e"], ["e", "ee", "s", "es"], ["s", "se"]], [], []);
|
|
30
|
+
_p("IN", [["e", "s"], ["e", "ee", "se"]], [], []);
|
|
31
|
+
_p("IK", [["e", "es", "s", "ss"], ["e", "ee", "se"]], [], ["s"]);
|
|
32
|
+
_p("IX", [["e", "s"], ["e", "ee", "se", "s", "es"]], [], []);
|
|
33
|
+
|
|
34
|
+
export function isOneWay(pat: HandshakePattern): boolean {
|
|
35
|
+
return pat.baseName.length === 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const NAME_RE = /^([NKX]|[NKXI]1?[NKX]1?)([a-z][a-z0-9]*(\+[a-z][a-z0-9]*)*)?$/;
|
|
39
|
+
const PSK_RE = /^psk([0-9]+)$/;
|
|
40
|
+
|
|
41
|
+
export function lookupPattern(name: string): HandshakePattern | null {
|
|
42
|
+
const m = NAME_RE.exec(name);
|
|
43
|
+
if (m === null) return null;
|
|
44
|
+
const modifiers = m[2]?.split('+') ?? [];
|
|
45
|
+
let pat: HandshakePattern | null = PATTERNS[m[1]] ?? null;
|
|
46
|
+
if (!pat) return null;
|
|
47
|
+
modifiers.forEach(m => pat = pat && applyModifier(pat, m));
|
|
48
|
+
return pat && { ... pat, name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function applyModifier(pat: HandshakePattern, mod: string): HandshakePattern | null {
|
|
52
|
+
const m = PSK_RE.exec(mod);
|
|
53
|
+
if (m === null) return null;
|
|
54
|
+
const n = parseInt(m[1], 10);
|
|
55
|
+
const messages = pat.messages;
|
|
56
|
+
return { ... pat, messages: (n === 0
|
|
57
|
+
? [["psk", ... messages[0]], ... messages.slice(1)]
|
|
58
|
+
: [... messages.slice(0, n-1), [... messages[n-1], "psk"], ... messages.slice(n)]) };
|
|
59
|
+
}
|