salty-crypto 0.0.5 → 0.1.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 +24 -21
- package/browser-demo.html +2 -2
- package/dist/salty-crypto.d.ts +277 -227
- package/dist/salty-crypto.js +1 -1
- package/package.json +1 -1
- package/speed.ts +22 -0
- package/src/aead/chacha20poly1305.ts +85 -0
- package/src/aead.ts +50 -94
- package/src/bytes.ts +29 -0
- package/src/{chacha20.ts → cipher/chacha20.ts} +37 -23
- package/src/cipher.ts +20 -0
- package/src/{x25519.ts → dh/x25519.ts} +0 -0
- package/src/dh.ts +30 -0
- package/src/{blake2.ts → hash/blake2s.ts} +11 -8
- package/src/{poly1305.ts → hash/poly1305.ts} +36 -29
- package/src/hash.ts +21 -0
- package/src/hkdf.ts +25 -0
- package/src/hmac.ts +24 -0
- package/src/index.ts +46 -9
- package/src/noise/algorithms.ts +26 -0
- package/src/noise/cipherstate.ts +38 -0
- package/src/noise/handshake.ts +240 -0
- package/src/{patterns.ts → noise/patterns.ts} +12 -1
- package/src/noise/profiles.ts +13 -0
- package/src/noise/rekey.ts +13 -0
- package/src/noise.ts +15 -365
- package/src/nonce.ts +23 -0
- package/test/tests/aead.test.ts +10 -10
- package/test/tests/blake2.test.ts +3 -4
- package/test/tests/chacha20.test.ts +11 -12
- package/test/tests/noise.test.ts +17 -19
- package/test/tests/poly1305.test.ts +2 -3
- package/src/profiles.ts +0 -59
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
// * https://github.com/floodyberry/poly1305-donna
|
|
10
10
|
// */
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import type { Hash, HashAlgorithm } from '../hash';
|
|
13
|
+
|
|
14
|
+
export const Poly1305 = (class Poly1305 implements HashAlgorithm {
|
|
15
|
+
static readonly NAME = "Poly1305";
|
|
13
16
|
static readonly KEYBYTES = 32;
|
|
14
|
-
static readonly
|
|
15
|
-
static readonly
|
|
17
|
+
static readonly OUTBYTES = 16;
|
|
18
|
+
static readonly BLOCKLEN = 16;
|
|
16
19
|
|
|
17
20
|
buffer = new Uint8Array(16);
|
|
18
21
|
r = new Uint16Array(10);
|
|
@@ -21,15 +24,16 @@ export class Poly1305 {
|
|
|
21
24
|
leftover = 0;
|
|
22
25
|
fin = 0;
|
|
23
26
|
|
|
24
|
-
static digest(
|
|
25
|
-
const p = new Poly1305(key);
|
|
26
|
-
p.update(input
|
|
27
|
-
|
|
28
|
-
p.finish(output, 0);
|
|
29
|
-
return output;
|
|
27
|
+
static digest(input: Uint8Array, key?: Uint8Array, outlen?: number): Uint8Array {
|
|
28
|
+
const p = new Poly1305(key, outlen);
|
|
29
|
+
p.update(input);
|
|
30
|
+
return p.final();
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
constructor(
|
|
33
|
+
constructor(key?: Uint8Array, outlen?: number) {
|
|
34
|
+
if (!key) throw new Error("Poly1305: key required");
|
|
35
|
+
if ((outlen ?? Poly1305.OUTBYTES) !== Poly1305.OUTBYTES) throw new Error("Poly1305: outlen != OUTBYTES");
|
|
36
|
+
|
|
33
37
|
const t0 = key[ 0] & 0xff | (key[ 1] & 0xff) << 8; this.r[0] = ( t0 ) & 0x1fff;
|
|
34
38
|
const t1 = key[ 2] & 0xff | (key[ 3] & 0xff) << 8; this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff;
|
|
35
39
|
const t2 = key[ 4] & 0xff | (key[ 5] & 0xff) << 8; this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03;
|
|
@@ -263,7 +267,9 @@ export class Poly1305 {
|
|
|
263
267
|
this.h[9] = h9;
|
|
264
268
|
}
|
|
265
269
|
|
|
266
|
-
|
|
270
|
+
final(mac?: Uint8Array): Uint8Array {
|
|
271
|
+
if (!mac) mac = new Uint8Array(Poly1305.OUTBYTES);
|
|
272
|
+
|
|
267
273
|
if (this.leftover) {
|
|
268
274
|
let i = this.leftover;
|
|
269
275
|
this.buffer[i++] = 1;
|
|
@@ -319,25 +325,26 @@ export class Poly1305 {
|
|
|
319
325
|
this.h[i] = f & 0xffff;
|
|
320
326
|
}
|
|
321
327
|
|
|
322
|
-
mac[
|
|
323
|
-
mac[
|
|
324
|
-
mac[
|
|
325
|
-
mac[
|
|
326
|
-
mac[
|
|
327
|
-
mac[
|
|
328
|
-
mac[
|
|
329
|
-
mac[
|
|
330
|
-
mac[
|
|
331
|
-
mac[
|
|
332
|
-
mac[
|
|
333
|
-
mac[
|
|
334
|
-
mac[
|
|
335
|
-
mac[
|
|
336
|
-
mac[
|
|
337
|
-
mac[
|
|
328
|
+
mac[0] = (this.h[0] >>> 0) & 0xff;
|
|
329
|
+
mac[1] = (this.h[0] >>> 8) & 0xff;
|
|
330
|
+
mac[2] = (this.h[1] >>> 0) & 0xff;
|
|
331
|
+
mac[3] = (this.h[1] >>> 8) & 0xff;
|
|
332
|
+
mac[4] = (this.h[2] >>> 0) & 0xff;
|
|
333
|
+
mac[5] = (this.h[2] >>> 8) & 0xff;
|
|
334
|
+
mac[6] = (this.h[3] >>> 0) & 0xff;
|
|
335
|
+
mac[7] = (this.h[3] >>> 8) & 0xff;
|
|
336
|
+
mac[8] = (this.h[4] >>> 0) & 0xff;
|
|
337
|
+
mac[9] = (this.h[4] >>> 8) & 0xff;
|
|
338
|
+
mac[10] = (this.h[5] >>> 0) & 0xff;
|
|
339
|
+
mac[11] = (this.h[5] >>> 8) & 0xff;
|
|
340
|
+
mac[12] = (this.h[6] >>> 0) & 0xff;
|
|
341
|
+
mac[13] = (this.h[6] >>> 8) & 0xff;
|
|
342
|
+
mac[14] = (this.h[7] >>> 0) & 0xff;
|
|
343
|
+
mac[15] = (this.h[7] >>> 8) & 0xff;
|
|
344
|
+
return mac;
|
|
338
345
|
};
|
|
339
346
|
|
|
340
|
-
update(m: Uint8Array, mpos
|
|
347
|
+
update(m: Uint8Array, mpos = 0, bytes = m.byteLength) {
|
|
341
348
|
if (this.leftover) {
|
|
342
349
|
let want = (16 - this.leftover);
|
|
343
350
|
if (want > bytes)
|
|
@@ -366,4 +373,4 @@ export class Poly1305 {
|
|
|
366
373
|
this.leftover += bytes;
|
|
367
374
|
}
|
|
368
375
|
}
|
|
369
|
-
}
|
|
376
|
+
}) satisfies Hash;
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
export interface Hash {
|
|
5
|
+
readonly NAME: string;
|
|
6
|
+
readonly KEYBYTES: number;
|
|
7
|
+
readonly OUTBYTES: number;
|
|
8
|
+
readonly BLOCKLEN: number;
|
|
9
|
+
|
|
10
|
+
digest(input: Uint8Array, key?: Uint8Array, outlen?: number): Uint8Array;
|
|
11
|
+
|
|
12
|
+
new(key?: Uint8Array, outlen?: number): HashAlgorithm;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HashAlgorithm {
|
|
16
|
+
update(input: Uint8Array, offset?: number, length?: number): void;
|
|
17
|
+
final(output?: Uint8Array): Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { BLAKE2s } from './hash/blake2s';
|
|
21
|
+
export { Poly1305 } from './hash/poly1305';
|
package/src/hkdf.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { HMAC } from './hmac';
|
|
5
|
+
import * as Bytes from './bytes';
|
|
6
|
+
|
|
7
|
+
export type HKDF = {
|
|
8
|
+
(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 2): [Uint8Array, Uint8Array];
|
|
9
|
+
(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 3): [Uint8Array, Uint8Array, Uint8Array];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function makeHKDF(hmac: HMAC): HKDF {
|
|
13
|
+
function hkdf(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 2): [Uint8Array, Uint8Array];
|
|
14
|
+
function hkdf(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 3): [Uint8Array, Uint8Array, Uint8Array];
|
|
15
|
+
function hkdf(chainingKey: Uint8Array, input: Uint8Array, numOutputs: 2 | 3): Uint8Array[] {
|
|
16
|
+
const tempKey = hmac(chainingKey, input);
|
|
17
|
+
const o1 = hmac(tempKey, Uint8Array.from([1]));
|
|
18
|
+
const o2 = hmac(tempKey, Bytes.append(o1, Uint8Array.from([2])));
|
|
19
|
+
switch (numOutputs) {
|
|
20
|
+
case 2: return [o1, o2];
|
|
21
|
+
case 3: return [o1, o2, hmac(tempKey, Bytes.append(o2, Uint8Array.from([3])))];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return hkdf;
|
|
25
|
+
}
|
package/src/hmac.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { Hash } from './hash';
|
|
5
|
+
import * as Bytes from './bytes';
|
|
6
|
+
|
|
7
|
+
export type HMAC = {
|
|
8
|
+
(key: Uint8Array, data: Uint8Array): Uint8Array;
|
|
9
|
+
readonly NAME: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function makeHMAC(hash: Hash): HMAC {
|
|
13
|
+
const HMAC_IPAD = new Uint8Array(hash.BLOCKLEN); HMAC_IPAD.fill(0x36);
|
|
14
|
+
const HMAC_OPAD = new Uint8Array(hash.BLOCKLEN); HMAC_OPAD.fill(0x5c);
|
|
15
|
+
const hmac = (key0: Uint8Array, data: Uint8Array) => {
|
|
16
|
+
const key1 = key0.byteLength > hash.BLOCKLEN ? hash.digest(key0) : key0;
|
|
17
|
+
const key = Bytes.append(key1, new Uint8Array(hash.BLOCKLEN - key1.byteLength));
|
|
18
|
+
return hash.digest(Bytes.append(Bytes.xor(key, HMAC_OPAD),
|
|
19
|
+
hash.digest(Bytes.append(Bytes.xor(key, HMAC_IPAD),
|
|
20
|
+
data))));
|
|
21
|
+
};
|
|
22
|
+
hmac.NAME = 'HMAC-' + hash.NAME;
|
|
23
|
+
return hmac;
|
|
24
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
/// SPDX-License-Identifier: MIT
|
|
2
2
|
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
3
|
|
|
4
|
-
export *
|
|
5
|
-
export * as
|
|
6
|
-
export *
|
|
7
|
-
export *
|
|
8
|
-
export *
|
|
9
|
-
export *
|
|
10
|
-
export *
|
|
11
|
-
export *
|
|
12
|
-
export *
|
|
4
|
+
export * from './aead';
|
|
5
|
+
export * as Bytes from './bytes';
|
|
6
|
+
export * from './cipher';
|
|
7
|
+
export * from './dh';
|
|
8
|
+
export * from './hash';
|
|
9
|
+
export * from './hkdf';
|
|
10
|
+
export * from './hmac';
|
|
11
|
+
export * from './noise';
|
|
12
|
+
export * from './nonce';
|
|
13
|
+
export * from './random';
|
|
14
|
+
|
|
15
|
+
import * as chacha20poly1305 from './aead/chacha20poly1305';
|
|
16
|
+
import * as chacha20 from './cipher/chacha20';
|
|
17
|
+
import * as x25519 from './dh/x25519';
|
|
18
|
+
import * as blake2s from './hash/blake2s';
|
|
19
|
+
import * as poly1305 from './hash/poly1305';
|
|
20
|
+
import * as algorithms from './noise/algorithms';
|
|
21
|
+
import * as cipherstate from './noise/cipherstate';
|
|
22
|
+
import * as handshake from './noise/handshake';
|
|
23
|
+
import * as patterns from './noise/patterns';
|
|
24
|
+
import * as profiles from './noise/profiles';
|
|
25
|
+
import * as rekey from './noise/rekey';
|
|
26
|
+
|
|
27
|
+
export const INTERNALS = {
|
|
28
|
+
aead: {
|
|
29
|
+
chacha20poly1305,
|
|
30
|
+
},
|
|
31
|
+
cipher: {
|
|
32
|
+
chacha20,
|
|
33
|
+
},
|
|
34
|
+
dh: {
|
|
35
|
+
x25519,
|
|
36
|
+
},
|
|
37
|
+
hash: {
|
|
38
|
+
blake2s,
|
|
39
|
+
poly1305,
|
|
40
|
+
},
|
|
41
|
+
noise: {
|
|
42
|
+
algorithms,
|
|
43
|
+
cipherstate,
|
|
44
|
+
handshake,
|
|
45
|
+
patterns,
|
|
46
|
+
profiles,
|
|
47
|
+
rekey,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { AEAD } from '../aead';
|
|
5
|
+
import { Hash } from '../hash';
|
|
6
|
+
import { DH } from '../dh';
|
|
7
|
+
import { HMAC } from '../hmac';
|
|
8
|
+
import { HKDF } from '../hkdf';
|
|
9
|
+
|
|
10
|
+
import { Rekey } from './rekey';
|
|
11
|
+
|
|
12
|
+
export interface Algorithms {
|
|
13
|
+
dh: DH,
|
|
14
|
+
aead: AEAD,
|
|
15
|
+
hash: Hash,
|
|
16
|
+
hmac?: HMAC,
|
|
17
|
+
hkdf?: HKDF,
|
|
18
|
+
rekey?: Rekey,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function matchPattern(a: Algorithms, protocol_name: string): string | null {
|
|
22
|
+
const r = new RegExp(`^Noise_([A-Za-z0-9+]+)_${a.dh.NAME}_${a.aead.NAME}_${a.hash.NAME}$`);
|
|
23
|
+
const m = r.exec(protocol_name);
|
|
24
|
+
if (m === null) return null;
|
|
25
|
+
return m[1];
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { Nonce } from '../nonce';
|
|
5
|
+
import { makeRekey } from './rekey';
|
|
6
|
+
import { Algorithms } from './algorithms';
|
|
7
|
+
|
|
8
|
+
export class CipherState {
|
|
9
|
+
view: DataView | null = null;
|
|
10
|
+
nonce = new Nonce();
|
|
11
|
+
|
|
12
|
+
constructor (public algorithms: Algorithms,
|
|
13
|
+
key?: Uint8Array)
|
|
14
|
+
{
|
|
15
|
+
if (key !== void 0) this.view = new DataView(key.buffer);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
encrypt(plaintext: Uint8Array, associated_data?: Uint8Array): Uint8Array {
|
|
19
|
+
if (this.view === null) return plaintext;
|
|
20
|
+
const ciphertext =
|
|
21
|
+
this.algorithms.aead.encrypt(plaintext, this.view, this.nonce, associated_data);
|
|
22
|
+
this.nonce.increment();
|
|
23
|
+
return ciphertext;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
decrypt(ciphertext: Uint8Array, associated_data?: Uint8Array): Uint8Array {
|
|
27
|
+
if (this.view === null) return ciphertext;
|
|
28
|
+
const plaintext =
|
|
29
|
+
this.algorithms.aead.decrypt(ciphertext, this.view, this.nonce, associated_data);
|
|
30
|
+
this.nonce.increment();
|
|
31
|
+
return plaintext;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rekey() {
|
|
35
|
+
if (this.view === null) return;
|
|
36
|
+
this.view = (this.algorithms.rekey ?? makeRekey(this.algorithms.aead))(this.view);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { DHKeyPair } from '../dh';
|
|
5
|
+
import * as Bytes from '../bytes';
|
|
6
|
+
|
|
7
|
+
import { Algorithms } from './algorithms';
|
|
8
|
+
import { CipherState } from './cipherstate';
|
|
9
|
+
import { HandshakePattern, KeyMixToken, Token } from './patterns';
|
|
10
|
+
import { HKDF, makeHKDF } from '../hkdf';
|
|
11
|
+
import { makeHMAC } from '../hmac';
|
|
12
|
+
|
|
13
|
+
export type Role = 'initiator' | 'responder';
|
|
14
|
+
|
|
15
|
+
export type HandshakeOptions = {
|
|
16
|
+
prologue?: Uint8Array,
|
|
17
|
+
staticKeypair?: DHKeyPair,
|
|
18
|
+
remoteStaticPublicKey?: Uint8Array,
|
|
19
|
+
pregeneratedEphemeralKeypair?: DHKeyPair,
|
|
20
|
+
remotePregeneratedEphemeralPublicKey?: Uint8Array,
|
|
21
|
+
preSharedKeys?: Uint8Array[],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TransportState = { send: CipherState, recv: CipherState };
|
|
25
|
+
|
|
26
|
+
export class Handshake {
|
|
27
|
+
staticKeypair: DHKeyPair;
|
|
28
|
+
remoteStaticPublicKey: Uint8Array | null;
|
|
29
|
+
ephemeralKeypair: DHKeyPair;
|
|
30
|
+
remoteEphemeralPublicKey: Uint8Array | null;
|
|
31
|
+
preSharedKeys?: Uint8Array[];
|
|
32
|
+
stepIndex = 0;
|
|
33
|
+
cipherState: CipherState;
|
|
34
|
+
chainingKey: Uint8Array;
|
|
35
|
+
handshakeHash: Uint8Array;
|
|
36
|
+
hkdf: HKDF;
|
|
37
|
+
|
|
38
|
+
constructor (public algorithms: Algorithms,
|
|
39
|
+
public pattern: HandshakePattern,
|
|
40
|
+
public role: Role,
|
|
41
|
+
options: HandshakeOptions = {})
|
|
42
|
+
{
|
|
43
|
+
this.staticKeypair = options.staticKeypair ?? this.algorithms.dh.generateKeypair();
|
|
44
|
+
this.remoteStaticPublicKey = options.remoteStaticPublicKey ?? null;
|
|
45
|
+
this.ephemeralKeypair = options.pregeneratedEphemeralKeypair ?? this.algorithms.dh.generateKeypair();
|
|
46
|
+
this.remoteEphemeralPublicKey = options.remotePregeneratedEphemeralPublicKey ?? null;
|
|
47
|
+
this.preSharedKeys = options.preSharedKeys;
|
|
48
|
+
if (this.preSharedKeys) {
|
|
49
|
+
this.preSharedKeys = this.preSharedKeys.slice();
|
|
50
|
+
if (this.preSharedKeys.length === 0) this.preSharedKeys = void 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const protocolName = new TextEncoder().encode(
|
|
54
|
+
'Noise_' + this.pattern.name +
|
|
55
|
+
'_' + this.algorithms.dh.NAME +
|
|
56
|
+
'_' + this.algorithms.aead.NAME +
|
|
57
|
+
'_' + this.algorithms.hash.NAME);
|
|
58
|
+
|
|
59
|
+
this.cipherState = new CipherState(this.algorithms);
|
|
60
|
+
{
|
|
61
|
+
const ckLen = this.algorithms.hash.OUTBYTES;
|
|
62
|
+
const ckSeed = (protocolName.byteLength > ckLen)
|
|
63
|
+
? this.algorithms.hash.digest(protocolName)
|
|
64
|
+
: protocolName;
|
|
65
|
+
this.chainingKey = Bytes.append(ckSeed, new Uint8Array(ckLen - ckSeed.byteLength));
|
|
66
|
+
}
|
|
67
|
+
this.handshakeHash = this.chainingKey;
|
|
68
|
+
|
|
69
|
+
this.mixHash(options.prologue ?? Bytes.EMPTY);
|
|
70
|
+
this.pattern.initiatorPreMessage.forEach(t => this.mixHash(t === 'e'
|
|
71
|
+
? (this.isInitiator ? this.ephemeralKeypair.public : this.remoteEphemeralPublicKey!)
|
|
72
|
+
: (this.isInitiator ? this.staticKeypair.public : this.remoteStaticPublicKey!)));
|
|
73
|
+
this.pattern.responderPreMessage.forEach(t => this.mixHash(t === 'e'
|
|
74
|
+
? (!this.isInitiator ? this.ephemeralKeypair.public : this.remoteEphemeralPublicKey!)
|
|
75
|
+
: (!this.isInitiator ? this.staticKeypair.public : this.remoteStaticPublicKey!)));
|
|
76
|
+
|
|
77
|
+
this.hkdf = this.algorithms.hkdf ?? makeHKDF(
|
|
78
|
+
this.algorithms.hmac ?? makeHMAC(this.algorithms.hash));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get isInitiator(): boolean {
|
|
82
|
+
return this.role === 'initiator';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mixHash(data: Uint8Array) {
|
|
86
|
+
this.handshakeHash = this.algorithms.hash.digest(Bytes.append(this.handshakeHash, data));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
mixKey(input: Uint8Array) {
|
|
90
|
+
const [newCk, k] = this.hkdf(this.chainingKey, input, 2);
|
|
91
|
+
this.chainingKey = newCk;
|
|
92
|
+
this.cipherState = new CipherState(this.algorithms, k);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
mixKeyAndHashNextPSK() {
|
|
96
|
+
const psk = this.preSharedKeys!.shift()!;
|
|
97
|
+
const [newCk, tempH, k] = this.hkdf(this.chainingKey, psk, 3);
|
|
98
|
+
this.chainingKey = newCk;
|
|
99
|
+
this.mixHash(tempH);
|
|
100
|
+
this.cipherState = new CipherState(this.algorithms, k);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
encryptAndHash(p: Uint8Array) {
|
|
104
|
+
const c = this.cipherState.encrypt(p, this.handshakeHash);
|
|
105
|
+
this.mixHash(c);
|
|
106
|
+
return c;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
decryptAndHash(c: Uint8Array) {
|
|
110
|
+
const p = this.cipherState.decrypt(c, this.handshakeHash);
|
|
111
|
+
this.mixHash(c);
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_split(): TransportState | null {
|
|
116
|
+
if (this.stepIndex < this.pattern.messages.length) {
|
|
117
|
+
return null;
|
|
118
|
+
} else {
|
|
119
|
+
let [kI, kR] = this.hkdf(this.chainingKey, Bytes.EMPTY, 2)
|
|
120
|
+
.map(k => new CipherState(this.algorithms, k));
|
|
121
|
+
return this.isInitiator ? { send: kI, recv: kR } : { send: kR, recv: kI };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_nextStep(): Token[] {
|
|
126
|
+
if (this.stepIndex >= this.pattern.messages.length) {
|
|
127
|
+
throw new Error("Handshake already complete, cannot continue");
|
|
128
|
+
}
|
|
129
|
+
return this.pattern.messages[this.stepIndex++];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_processKeyMixToken(t: KeyMixToken) {
|
|
133
|
+
switch (t) {
|
|
134
|
+
case 'ee':
|
|
135
|
+
this.mixKey(this.algorithms.dh.dh(this.ephemeralKeypair, this.remoteEphemeralPublicKey!));
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'es':
|
|
139
|
+
this.mixKey(this.isInitiator
|
|
140
|
+
? this.algorithms.dh.dh(this.ephemeralKeypair, this.remoteStaticPublicKey!)
|
|
141
|
+
: this.algorithms.dh.dh(this.staticKeypair, this.remoteEphemeralPublicKey!));
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'se':
|
|
145
|
+
this.mixKey(!this.isInitiator
|
|
146
|
+
? this.algorithms.dh.dh(this.ephemeralKeypair, this.remoteStaticPublicKey!)
|
|
147
|
+
: this.algorithms.dh.dh(this.staticKeypair, this.remoteEphemeralPublicKey!));
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case 'ss':
|
|
151
|
+
this.mixKey(this.algorithms.dh.dh(this.staticKeypair, this.remoteStaticPublicKey!));
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'psk':
|
|
155
|
+
this.mixKeyAndHashNextPSK();
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
writeMessage(payload: Uint8Array): { packet: Uint8Array, finished: TransportState | null } {
|
|
161
|
+
const pieces = [];
|
|
162
|
+
this._nextStep().forEach(t => {
|
|
163
|
+
switch (t) {
|
|
164
|
+
case 'e':
|
|
165
|
+
pieces.push(this.ephemeralKeypair.public);
|
|
166
|
+
this.mixHash(this.ephemeralKeypair.public);
|
|
167
|
+
if (this.preSharedKeys) this.mixKey(this.ephemeralKeypair.public);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 's':
|
|
171
|
+
pieces.push(this.encryptAndHash(this.staticKeypair.public));
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
default:
|
|
175
|
+
this._processKeyMixToken(t);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
pieces.push(this.encryptAndHash(payload));
|
|
180
|
+
|
|
181
|
+
let packet: Uint8Array;
|
|
182
|
+
if (pieces.length === 1) {
|
|
183
|
+
packet = pieces[0];
|
|
184
|
+
} else {
|
|
185
|
+
packet = new Uint8Array(pieces.reduce((ac, p) => ac + p.byteLength, 0));
|
|
186
|
+
let offset = 0;
|
|
187
|
+
pieces.forEach(p => { packet.set(p, offset); offset += p.byteLength; });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { packet, finished: this._split() };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
readMessage(packet: Uint8Array): { message: Uint8Array, finished: TransportState | null } {
|
|
194
|
+
const take = (n: number): Uint8Array => {
|
|
195
|
+
const bs = packet.slice(0, n);
|
|
196
|
+
packet = packet.subarray(n);
|
|
197
|
+
return bs;
|
|
198
|
+
};
|
|
199
|
+
this._nextStep().forEach(t => {
|
|
200
|
+
switch (t) {
|
|
201
|
+
case 'e':
|
|
202
|
+
this.remoteEphemeralPublicKey = take(this.algorithms.dh.DHLEN);
|
|
203
|
+
this.mixHash(this.remoteEphemeralPublicKey);
|
|
204
|
+
if (this.preSharedKeys) this.mixKey(this.remoteEphemeralPublicKey);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 's':
|
|
208
|
+
this.remoteStaticPublicKey = this.decryptAndHash(take(
|
|
209
|
+
this.algorithms.dh.DHLEN + (this.cipherState.view ? 16 : 0)));
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
default:
|
|
213
|
+
this._processKeyMixToken(t);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const message = this.decryptAndHash(packet);
|
|
219
|
+
return { message, finished: this._split() };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async completeHandshake(writePacket: (packet: Uint8Array) => Promise<void>,
|
|
223
|
+
readPacket: () => Promise<Uint8Array>,
|
|
224
|
+
handleMessage = async (_m: Uint8Array): Promise<void> => {},
|
|
225
|
+
produceMessage = async (): Promise<Uint8Array> => new Uint8Array(0))
|
|
226
|
+
: Promise<TransportState>
|
|
227
|
+
{
|
|
228
|
+
const W = async (): Promise<TransportState> => {
|
|
229
|
+
const { packet, finished } = this.writeMessage(await produceMessage());
|
|
230
|
+
await writePacket(packet);
|
|
231
|
+
return finished || R();
|
|
232
|
+
};
|
|
233
|
+
const R = async (): Promise<TransportState> => {
|
|
234
|
+
const { message, finished } = this.readMessage(await readPacket());
|
|
235
|
+
await handleMessage(message);
|
|
236
|
+
return finished || W();
|
|
237
|
+
};
|
|
238
|
+
return (this.isInitiator ? W() : R());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/// SPDX-License-Identifier: MIT
|
|
2
2
|
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export type KeyTransferToken = 'e' | 's';
|
|
5
|
+
export type KeyMixToken = 'ee' | 'es' | 'se' | 'ss' | 'psk';
|
|
6
|
+
export type Token = KeyTransferToken | KeyMixToken;
|
|
7
|
+
export type PreMessage = ['e'] | ['s'] | ['e', 's'] | [];
|
|
8
|
+
|
|
9
|
+
export interface HandshakePattern {
|
|
10
|
+
name: string; // e.g. "NNpsk2"
|
|
11
|
+
baseName: string; // e.g. "NN"
|
|
12
|
+
messages: Token[][];
|
|
13
|
+
initiatorPreMessage: PreMessage;
|
|
14
|
+
responderPreMessage: PreMessage;
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
export const PATTERNS: { [key: string]: HandshakePattern } = {};
|
|
7
18
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { Algorithms } from './algorithms';
|
|
5
|
+
import { BLAKE2s } from '../hash';
|
|
6
|
+
import { ChaCha20Poly1305_RFC8439 } from '../aead';
|
|
7
|
+
import { X25519 } from '../dh';
|
|
8
|
+
|
|
9
|
+
export const Noise_25519_ChaChaPoly_BLAKE2s: Algorithms = {
|
|
10
|
+
dh: X25519,
|
|
11
|
+
aead: ChaCha20Poly1305_RFC8439,
|
|
12
|
+
hash: BLAKE2s,
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// SPDX-License-Identifier: MIT
|
|
2
|
+
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
3
|
+
|
|
4
|
+
import { AEAD } from '../aead';
|
|
5
|
+
import { Nonce } from '../nonce';
|
|
6
|
+
|
|
7
|
+
export type Rekey = (k: DataView) => DataView;
|
|
8
|
+
|
|
9
|
+
export function makeRekey(aead: AEAD): Rekey {
|
|
10
|
+
return (k: DataView): DataView => {
|
|
11
|
+
return new DataView(aead.encrypt(new Uint8Array(32), k, Nonce.MAX).buffer);
|
|
12
|
+
};
|
|
13
|
+
}
|