@libp2p/pnet 1.0.0-01e9a5fe4
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/LICENSE +4 -0
- package/README.md +93 -0
- package/dist/index.min.js +23 -0
- package/dist/src/crypto.d.ts +19 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +56 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/errors.d.ts +7 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +7 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +76 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +124 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/key-generator.d.ts +11 -0
- package/dist/src/key-generator.d.ts.map +1 -0
- package/dist/src/key-generator.js +22 -0
- package/dist/src/key-generator.js.map +1 -0
- package/package.json +72 -0
- package/src/crypto.ts +63 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +167 -0
- package/src/key-generator.ts +23 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
|
3
|
+
import xsalsa20 from 'xsalsa20';
|
4
|
+
import * as Errors from './errors.js';
|
5
|
+
import { KEY_LENGTH } from './key-generator.js';
|
6
|
+
/**
|
7
|
+
* Creates a stream iterable to encrypt messages in a private network
|
8
|
+
*/
|
9
|
+
export function createBoxStream(nonce, psk) {
|
10
|
+
const xor = xsalsa20(nonce, psk);
|
11
|
+
return (source) => (async function* () {
|
12
|
+
for await (const chunk of source) {
|
13
|
+
yield Uint8Array.from(xor.update(chunk.subarray()));
|
14
|
+
}
|
15
|
+
})();
|
16
|
+
}
|
17
|
+
/**
|
18
|
+
* Creates a stream iterable to decrypt messages in a private network
|
19
|
+
*/
|
20
|
+
export function createUnboxStream(nonce, psk) {
|
21
|
+
return (source) => (async function* () {
|
22
|
+
const xor = xsalsa20(nonce, psk);
|
23
|
+
for await (const chunk of source) {
|
24
|
+
yield Uint8Array.from(xor.update(chunk.subarray()));
|
25
|
+
}
|
26
|
+
})();
|
27
|
+
}
|
28
|
+
/**
|
29
|
+
* Decode the version 1 psk from the given Uint8Array
|
30
|
+
*/
|
31
|
+
export function decodeV1PSK(pskBuffer) {
|
32
|
+
try {
|
33
|
+
// This should pull from multibase/multicodec to allow for
|
34
|
+
// more encoding flexibility. Ideally we'd consume the codecs
|
35
|
+
// from the buffer line by line to evaluate the next line
|
36
|
+
// programmatically instead of making assumptions about the
|
37
|
+
// encodings of each line.
|
38
|
+
const metadata = uint8ArrayToString(pskBuffer).split(/(?:\r\n|\r|\n)/g);
|
39
|
+
const pskTag = metadata.shift();
|
40
|
+
const codec = metadata.shift();
|
41
|
+
const pskString = metadata.shift();
|
42
|
+
const psk = uint8ArrayFromString(pskString ?? '', 'base16');
|
43
|
+
if (psk.byteLength !== KEY_LENGTH) {
|
44
|
+
throw new Error(Errors.INVALID_PSK);
|
45
|
+
}
|
46
|
+
return {
|
47
|
+
tag: pskTag,
|
48
|
+
codecName: codec,
|
49
|
+
psk
|
50
|
+
};
|
51
|
+
}
|
52
|
+
catch (err) {
|
53
|
+
throw new Error(Errors.INVALID_PSK);
|
54
|
+
}
|
55
|
+
}
|
56
|
+
//# sourceMappingURL=crypto.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAC5E,OAAO,EAAE,QAAQ,IAAI,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AACtE,OAAO,QAAQ,MAAM,UAAU,CAAA;AAC/B,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAI/C;;GAEG;AACH,MAAM,UAAU,eAAe,CAAE,KAAiB,EAAE,GAAe;IACjE,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAEhC,OAAO,CAAC,MAA2C,EAAE,EAAE,CAAC,CAAC,KAAK,SAAU,CAAC;QACvE,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACjC,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;QACrD,CAAC;IACH,CAAC,CAAC,EAAE,CAAA;AACN,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAE,KAAiB,EAAE,GAAe;IACnE,OAAO,CAAC,MAA0B,EAAE,EAAE,CAAC,CAAC,KAAK,SAAU,CAAC;QACtD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;QAEhC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACjC,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;QACrD,CAAC;IACH,CAAC,CAAC,EAAE,CAAA;AACN,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAE,SAAqB;IAChD,IAAI,CAAC;QACH,0DAA0D;QAC1D,6DAA6D;QAC7D,yDAAyD;QACzD,2DAA2D;QAC3D,0BAA0B;QAC1B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACvE,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAA;QAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAA;QAC9B,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAA;QAClC,MAAM,GAAG,GAAG,oBAAoB,CAAC,SAAS,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAA;QAE3D,IAAI,GAAG,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QACrC,CAAC;QAED,OAAO;YACL,GAAG,EAAE,MAAM;YACX,SAAS,EAAE,KAAK;YAChB,GAAG;SACJ,CAAA;IACH,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACrC,CAAC;AACH,CAAC"}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
export declare const INVALID_PEER = "Not a valid peer connection";
|
2
|
+
export declare const INVALID_PSK = "Your private shared key is invalid";
|
3
|
+
export declare const NO_LOCAL_ID = "No local private key provided";
|
4
|
+
export declare const NO_HANDSHAKE_CONNECTION = "No connection for the handshake provided";
|
5
|
+
export declare const STREAM_ENDED = "Stream ended prematurely";
|
6
|
+
export declare const ERR_INVALID_PARAMETERS = "ERR_INVALID_PARAMETERS";
|
7
|
+
//# sourceMappingURL=errors.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,gCAAgC,CAAA;AACzD,eAAO,MAAM,WAAW,uCAAuC,CAAA;AAC/D,eAAO,MAAM,WAAW,kCAAkC,CAAA;AAC1D,eAAO,MAAM,uBAAuB,6CAA6C,CAAA;AACjF,eAAO,MAAM,YAAY,6BAA6B,CAAA;AACtD,eAAO,MAAM,sBAAsB,2BAA2B,CAAA"}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
export const INVALID_PEER = 'Not a valid peer connection';
|
2
|
+
export const INVALID_PSK = 'Your private shared key is invalid';
|
3
|
+
export const NO_LOCAL_ID = 'No local private key provided';
|
4
|
+
export const NO_HANDSHAKE_CONNECTION = 'No connection for the handshake provided';
|
5
|
+
export const STREAM_ENDED = 'Stream ended prematurely';
|
6
|
+
export const ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS';
|
7
|
+
//# sourceMappingURL=errors.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,6BAA6B,CAAA;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,oCAAoC,CAAA;AAC/D,MAAM,CAAC,MAAM,WAAW,GAAG,+BAA+B,CAAA;AAC1D,MAAM,CAAC,MAAM,uBAAuB,GAAG,0CAA0C,CAAA;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,0BAA0B,CAAA;AACtD,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAA"}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
/**
|
2
|
+
* @packageDocumentation
|
3
|
+
*
|
4
|
+
* Connection protection management for libp2p leveraging PSK encryption via XSalsa20.
|
5
|
+
*
|
6
|
+
* @example
|
7
|
+
*
|
8
|
+
* ```typescript
|
9
|
+
* import { createLibp2p } from 'libp2p'
|
10
|
+
* import { preSharedKey, generateKey } from '@libp2p/pnet'
|
11
|
+
*
|
12
|
+
* // Create a Uint8Array and write the swarm key to it
|
13
|
+
* const swarmKey = new Uint8Array(95)
|
14
|
+
* generateKey(swarmKey)
|
15
|
+
*
|
16
|
+
* const node = await createLibp2p({
|
17
|
+
* // ...other options
|
18
|
+
* connectionProtector: preSharedKey({
|
19
|
+
* psk: swarmKey
|
20
|
+
* })
|
21
|
+
* })
|
22
|
+
* ```
|
23
|
+
*
|
24
|
+
* ## Private Shared Keys
|
25
|
+
*
|
26
|
+
* Private Shared Keys are expected to be in the following format:
|
27
|
+
*
|
28
|
+
* ```
|
29
|
+
* /key/swarm/psk/1.0.0/
|
30
|
+
* /base16/
|
31
|
+
* dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d
|
32
|
+
* ```
|
33
|
+
*
|
34
|
+
* ## PSK Generation
|
35
|
+
*
|
36
|
+
* A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key.
|
37
|
+
*
|
38
|
+
* ### From a module using libp2p
|
39
|
+
*
|
40
|
+
* If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node_modules are installed.
|
41
|
+
*
|
42
|
+
* ```console
|
43
|
+
* node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key
|
44
|
+
* ```
|
45
|
+
*
|
46
|
+
* ### Programmatically
|
47
|
+
*
|
48
|
+
* ```js
|
49
|
+
* import fs from 'fs'
|
50
|
+
* import { generateKey } from '@libp2p/pnet'
|
51
|
+
*
|
52
|
+
* const swarmKey = new Uint8Array(95)
|
53
|
+
* generateKey(swarmKey)
|
54
|
+
*
|
55
|
+
* fs.writeFileSync('swarm.key', swarmKey)
|
56
|
+
* ```
|
57
|
+
*/
|
58
|
+
import type { ComponentLogger, ConnectionProtector } from '@libp2p/interface';
|
59
|
+
export { generateKey } from './key-generator.js';
|
60
|
+
export interface ProtectorInit {
|
61
|
+
/**
|
62
|
+
* A pre-shared key. This must be the same byte value for all peers in the
|
63
|
+
* swarm in order for them to communicate.
|
64
|
+
*/
|
65
|
+
psk: Uint8Array;
|
66
|
+
/**
|
67
|
+
* The initial nonce exchange must complete within this many milliseconds
|
68
|
+
* (default: 1000)
|
69
|
+
*/
|
70
|
+
timeout?: number;
|
71
|
+
}
|
72
|
+
export interface ProtectorComponents {
|
73
|
+
logger: ComponentLogger;
|
74
|
+
}
|
75
|
+
export declare function preSharedKey(init: ProtectorInit): (components: ProtectorComponents) => ConnectionProtector;
|
76
|
+
//# sourceMappingURL=index.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAeH,OAAO,KAAK,EAAE,eAAe,EAAU,mBAAmB,EAAuB,MAAM,mBAAmB,CAAA;AAG1G,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,GAAG,EAAE,UAAU,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,eAAe,CAAA;CACxB;AAyED,wBAAgB,YAAY,CAAE,IAAI,EAAE,aAAa,GAAG,CAAC,UAAU,EAAE,mBAAmB,KAAK,mBAAmB,CAE3G"}
|
@@ -0,0 +1,124 @@
|
|
1
|
+
/**
|
2
|
+
* @packageDocumentation
|
3
|
+
*
|
4
|
+
* Connection protection management for libp2p leveraging PSK encryption via XSalsa20.
|
5
|
+
*
|
6
|
+
* @example
|
7
|
+
*
|
8
|
+
* ```typescript
|
9
|
+
* import { createLibp2p } from 'libp2p'
|
10
|
+
* import { preSharedKey, generateKey } from '@libp2p/pnet'
|
11
|
+
*
|
12
|
+
* // Create a Uint8Array and write the swarm key to it
|
13
|
+
* const swarmKey = new Uint8Array(95)
|
14
|
+
* generateKey(swarmKey)
|
15
|
+
*
|
16
|
+
* const node = await createLibp2p({
|
17
|
+
* // ...other options
|
18
|
+
* connectionProtector: preSharedKey({
|
19
|
+
* psk: swarmKey
|
20
|
+
* })
|
21
|
+
* })
|
22
|
+
* ```
|
23
|
+
*
|
24
|
+
* ## Private Shared Keys
|
25
|
+
*
|
26
|
+
* Private Shared Keys are expected to be in the following format:
|
27
|
+
*
|
28
|
+
* ```
|
29
|
+
* /key/swarm/psk/1.0.0/
|
30
|
+
* /base16/
|
31
|
+
* dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d
|
32
|
+
* ```
|
33
|
+
*
|
34
|
+
* ## PSK Generation
|
35
|
+
*
|
36
|
+
* A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key.
|
37
|
+
*
|
38
|
+
* ### From a module using libp2p
|
39
|
+
*
|
40
|
+
* If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node_modules are installed.
|
41
|
+
*
|
42
|
+
* ```console
|
43
|
+
* node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key
|
44
|
+
* ```
|
45
|
+
*
|
46
|
+
* ### Programmatically
|
47
|
+
*
|
48
|
+
* ```js
|
49
|
+
* import fs from 'fs'
|
50
|
+
* import { generateKey } from '@libp2p/pnet'
|
51
|
+
*
|
52
|
+
* const swarmKey = new Uint8Array(95)
|
53
|
+
* generateKey(swarmKey)
|
54
|
+
*
|
55
|
+
* fs.writeFileSync('swarm.key', swarmKey)
|
56
|
+
* ```
|
57
|
+
*/
|
58
|
+
import { randomBytes } from '@libp2p/crypto';
|
59
|
+
import { CodeError } from '@libp2p/interface';
|
60
|
+
import { byteStream } from 'it-byte-stream';
|
61
|
+
import map from 'it-map';
|
62
|
+
import { duplexPair } from 'it-pair/duplex';
|
63
|
+
import { pipe } from 'it-pipe';
|
64
|
+
import { createBoxStream, createUnboxStream, decodeV1PSK } from './crypto.js';
|
65
|
+
import * as Errors from './errors.js';
|
66
|
+
import { NONCE_LENGTH } from './key-generator.js';
|
67
|
+
export { generateKey } from './key-generator.js';
|
68
|
+
class PreSharedKeyConnectionProtector {
|
69
|
+
tag;
|
70
|
+
log;
|
71
|
+
psk;
|
72
|
+
timeout;
|
73
|
+
/**
|
74
|
+
* Takes a Private Shared Key (psk) and provides a `protect` method
|
75
|
+
* for wrapping existing connections in a private encryption stream.
|
76
|
+
*/
|
77
|
+
constructor(components, init) {
|
78
|
+
this.log = components.logger.forComponent('libp2p:pnet');
|
79
|
+
this.timeout = init.timeout ?? 1000;
|
80
|
+
const decodedPSK = decodeV1PSK(init.psk);
|
81
|
+
this.psk = decodedPSK.psk;
|
82
|
+
this.tag = decodedPSK.tag ?? '';
|
83
|
+
}
|
84
|
+
/**
|
85
|
+
* Takes a given Connection and creates a private encryption stream
|
86
|
+
* between its two peers from the PSK the Protector instance was
|
87
|
+
* created with.
|
88
|
+
*/
|
89
|
+
async protect(connection) {
|
90
|
+
if (connection == null) {
|
91
|
+
throw new CodeError(Errors.NO_HANDSHAKE_CONNECTION, Errors.ERR_INVALID_PARAMETERS);
|
92
|
+
}
|
93
|
+
// Exchange nonces
|
94
|
+
this.log('protecting the connection');
|
95
|
+
const localNonce = randomBytes(NONCE_LENGTH);
|
96
|
+
const signal = AbortSignal.timeout(this.timeout);
|
97
|
+
const bytes = byteStream(connection);
|
98
|
+
const [, result] = await Promise.all([
|
99
|
+
bytes.write(localNonce, {
|
100
|
+
signal
|
101
|
+
}),
|
102
|
+
bytes.read(NONCE_LENGTH, {
|
103
|
+
signal
|
104
|
+
})
|
105
|
+
]);
|
106
|
+
const remoteNonce = result.subarray();
|
107
|
+
// Create the boxing/unboxing pipe
|
108
|
+
this.log('exchanged nonces');
|
109
|
+
const [internal, external] = duplexPair();
|
110
|
+
pipe(external,
|
111
|
+
// Encrypt all outbound traffic
|
112
|
+
createBoxStream(localNonce, this.psk), bytes.unwrap(), (source) => map(source, (buf) => buf.subarray()),
|
113
|
+
// Decrypt all inbound traffic
|
114
|
+
createUnboxStream(remoteNonce, this.psk), external).catch(this.log.error);
|
115
|
+
return {
|
116
|
+
...connection,
|
117
|
+
...internal
|
118
|
+
};
|
119
|
+
}
|
120
|
+
}
|
121
|
+
export function preSharedKey(init) {
|
122
|
+
return (components) => new PreSharedKeyConnectionProtector(components, init);
|
123
|
+
}
|
124
|
+
//# sourceMappingURL=index.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
/// <reference types="node" />
|
2
|
+
/**
|
3
|
+
* Generates a PSK that can be used in a libp2p-pnet private network
|
4
|
+
*
|
5
|
+
* @param {Uint8Array | NodeJS.WriteStream} bytes - An object to write the psk into
|
6
|
+
* @returns {void}
|
7
|
+
*/
|
8
|
+
export declare function generateKey(bytes: Uint8Array | NodeJS.WriteStream): void;
|
9
|
+
export declare const NONCE_LENGTH = 24;
|
10
|
+
export declare const KEY_LENGTH = 32;
|
11
|
+
//# sourceMappingURL=key-generator.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"key-generator.d.ts","sourceRoot":"","sources":["../../src/key-generator.ts"],"names":[],"mappings":";AAIA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,WAAW,GAAG,IAAI,CASzE;AAED,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B,eAAO,MAAM,UAAU,KAAK,CAAA"}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { randomBytes } from '@libp2p/crypto';
|
2
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
|
3
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
|
4
|
+
/**
|
5
|
+
* Generates a PSK that can be used in a libp2p-pnet private network
|
6
|
+
*
|
7
|
+
* @param {Uint8Array | NodeJS.WriteStream} bytes - An object to write the psk into
|
8
|
+
* @returns {void}
|
9
|
+
*/
|
10
|
+
export function generateKey(bytes) {
|
11
|
+
const psk = uint8ArrayToString(randomBytes(KEY_LENGTH), 'base16');
|
12
|
+
const key = uint8ArrayFromString('/key/swarm/psk/1.0.0/\n/base16/\n' + psk);
|
13
|
+
if (bytes instanceof Uint8Array) {
|
14
|
+
bytes.set(key);
|
15
|
+
}
|
16
|
+
else {
|
17
|
+
bytes.write(key);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
export const NONCE_LENGTH = 24;
|
21
|
+
export const KEY_LENGTH = 32;
|
22
|
+
//# sourceMappingURL=key-generator.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"key-generator.js","sourceRoot":"","sources":["../../src/key-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAC5E,OAAO,EAAE,QAAQ,IAAI,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAEtE;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAE,KAAsC;IACjE,MAAM,GAAG,GAAG,kBAAkB,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,CAAA;IACjE,MAAM,GAAG,GAAG,oBAAoB,CAAC,mCAAmC,GAAG,GAAG,CAAC,CAAA;IAE3E,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAChC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChB,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAClB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAA;AAC9B,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAA"}
|
package/package.json
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
{
|
2
|
+
"name": "@libp2p/pnet",
|
3
|
+
"version": "1.0.0-01e9a5fe4",
|
4
|
+
"description": "Implementation of Connection protection management via a shared secret",
|
5
|
+
"license": "Apache-2.0 OR MIT",
|
6
|
+
"homepage": "https://github.com/libp2p/js-libp2p/tree/main/packages/pnet#readme",
|
7
|
+
"repository": {
|
8
|
+
"type": "git",
|
9
|
+
"url": "git+https://github.com/libp2p/js-libp2p.git"
|
10
|
+
},
|
11
|
+
"bugs": {
|
12
|
+
"url": "https://github.com/libp2p/js-libp2p/issues"
|
13
|
+
},
|
14
|
+
"publishConfig": {
|
15
|
+
"access": "public",
|
16
|
+
"provenance": true
|
17
|
+
},
|
18
|
+
"type": "module",
|
19
|
+
"types": "./dist/src/index.d.ts",
|
20
|
+
"files": [
|
21
|
+
"src",
|
22
|
+
"dist",
|
23
|
+
"!dist/test",
|
24
|
+
"!**/*.tsbuildinfo"
|
25
|
+
],
|
26
|
+
"exports": {
|
27
|
+
".": {
|
28
|
+
"types": "./dist/src/index.d.ts",
|
29
|
+
"import": "./dist/src/index.js"
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"eslintConfig": {
|
33
|
+
"extends": "ipfs",
|
34
|
+
"parserOptions": {
|
35
|
+
"project": true,
|
36
|
+
"sourceType": "module"
|
37
|
+
}
|
38
|
+
},
|
39
|
+
"scripts": {
|
40
|
+
"build": "aegir build",
|
41
|
+
"test": "aegir test",
|
42
|
+
"clean": "aegir clean",
|
43
|
+
"lint": "aegir lint",
|
44
|
+
"test:chrome": "aegir test -t browser --cov",
|
45
|
+
"test:chrome-webworker": "aegir test -t webworker",
|
46
|
+
"test:firefox": "aegir test -t browser -- --browser firefox",
|
47
|
+
"test:firefox-webworker": "aegir test -t webworker -- --browser firefox",
|
48
|
+
"test:node": "aegir test -t node --cov",
|
49
|
+
"dep-check": "aegir dep-check"
|
50
|
+
},
|
51
|
+
"dependencies": {
|
52
|
+
"@libp2p/crypto": "3.0.2-01e9a5fe4",
|
53
|
+
"@libp2p/interface": "1.0.2-01e9a5fe4",
|
54
|
+
"it-byte-stream": "^1.0.5",
|
55
|
+
"it-map": "^3.0.4",
|
56
|
+
"it-pair": "^2.0.6",
|
57
|
+
"it-pipe": "^3.0.1",
|
58
|
+
"it-stream-types": "^2.0.1",
|
59
|
+
"uint8arraylist": "^2.4.7",
|
60
|
+
"uint8arrays": "^5.0.0",
|
61
|
+
"xsalsa20": "^1.1.0"
|
62
|
+
},
|
63
|
+
"devDependencies": {
|
64
|
+
"@libp2p/interface-compliance-tests": "5.0.7-01e9a5fe4",
|
65
|
+
"@libp2p/logger": "4.0.2-01e9a5fe4",
|
66
|
+
"@libp2p/peer-id-factory": "4.0.1-01e9a5fe4",
|
67
|
+
"@multiformats/multiaddr": "^12.1.10",
|
68
|
+
"@types/xsalsa20": "^1.1.0",
|
69
|
+
"aegir": "^41.0.2",
|
70
|
+
"it-all": "^3.0.3"
|
71
|
+
}
|
72
|
+
}
|
package/src/crypto.ts
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
3
|
+
import xsalsa20 from 'xsalsa20'
|
4
|
+
import * as Errors from './errors.js'
|
5
|
+
import { KEY_LENGTH } from './key-generator.js'
|
6
|
+
import type { Source } from 'it-stream-types'
|
7
|
+
import type { Uint8ArrayList } from 'uint8arraylist'
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Creates a stream iterable to encrypt messages in a private network
|
11
|
+
*/
|
12
|
+
export function createBoxStream (nonce: Uint8Array, psk: Uint8Array): (source: Source<Uint8Array | Uint8ArrayList>) => AsyncGenerator<Uint8Array | Uint8ArrayList> {
|
13
|
+
const xor = xsalsa20(nonce, psk)
|
14
|
+
|
15
|
+
return (source: Source<Uint8Array | Uint8ArrayList>) => (async function * () {
|
16
|
+
for await (const chunk of source) {
|
17
|
+
yield Uint8Array.from(xor.update(chunk.subarray()))
|
18
|
+
}
|
19
|
+
})()
|
20
|
+
}
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Creates a stream iterable to decrypt messages in a private network
|
24
|
+
*/
|
25
|
+
export function createUnboxStream (nonce: Uint8Array, psk: Uint8Array) {
|
26
|
+
return (source: Source<Uint8Array>) => (async function * () {
|
27
|
+
const xor = xsalsa20(nonce, psk)
|
28
|
+
|
29
|
+
for await (const chunk of source) {
|
30
|
+
yield Uint8Array.from(xor.update(chunk.subarray()))
|
31
|
+
}
|
32
|
+
})()
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Decode the version 1 psk from the given Uint8Array
|
37
|
+
*/
|
38
|
+
export function decodeV1PSK (pskBuffer: Uint8Array): { tag: string | undefined, codecName: string | undefined, psk: Uint8Array } {
|
39
|
+
try {
|
40
|
+
// This should pull from multibase/multicodec to allow for
|
41
|
+
// more encoding flexibility. Ideally we'd consume the codecs
|
42
|
+
// from the buffer line by line to evaluate the next line
|
43
|
+
// programmatically instead of making assumptions about the
|
44
|
+
// encodings of each line.
|
45
|
+
const metadata = uint8ArrayToString(pskBuffer).split(/(?:\r\n|\r|\n)/g)
|
46
|
+
const pskTag = metadata.shift()
|
47
|
+
const codec = metadata.shift()
|
48
|
+
const pskString = metadata.shift()
|
49
|
+
const psk = uint8ArrayFromString(pskString ?? '', 'base16')
|
50
|
+
|
51
|
+
if (psk.byteLength !== KEY_LENGTH) {
|
52
|
+
throw new Error(Errors.INVALID_PSK)
|
53
|
+
}
|
54
|
+
|
55
|
+
return {
|
56
|
+
tag: pskTag,
|
57
|
+
codecName: codec,
|
58
|
+
psk
|
59
|
+
}
|
60
|
+
} catch (err: any) {
|
61
|
+
throw new Error(Errors.INVALID_PSK)
|
62
|
+
}
|
63
|
+
}
|
package/src/errors.ts
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
export const INVALID_PEER = 'Not a valid peer connection'
|
2
|
+
export const INVALID_PSK = 'Your private shared key is invalid'
|
3
|
+
export const NO_LOCAL_ID = 'No local private key provided'
|
4
|
+
export const NO_HANDSHAKE_CONNECTION = 'No connection for the handshake provided'
|
5
|
+
export const STREAM_ENDED = 'Stream ended prematurely'
|
6
|
+
export const ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS'
|
package/src/index.ts
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
/**
|
2
|
+
* @packageDocumentation
|
3
|
+
*
|
4
|
+
* Connection protection management for libp2p leveraging PSK encryption via XSalsa20.
|
5
|
+
*
|
6
|
+
* @example
|
7
|
+
*
|
8
|
+
* ```typescript
|
9
|
+
* import { createLibp2p } from 'libp2p'
|
10
|
+
* import { preSharedKey, generateKey } from '@libp2p/pnet'
|
11
|
+
*
|
12
|
+
* // Create a Uint8Array and write the swarm key to it
|
13
|
+
* const swarmKey = new Uint8Array(95)
|
14
|
+
* generateKey(swarmKey)
|
15
|
+
*
|
16
|
+
* const node = await createLibp2p({
|
17
|
+
* // ...other options
|
18
|
+
* connectionProtector: preSharedKey({
|
19
|
+
* psk: swarmKey
|
20
|
+
* })
|
21
|
+
* })
|
22
|
+
* ```
|
23
|
+
*
|
24
|
+
* ## Private Shared Keys
|
25
|
+
*
|
26
|
+
* Private Shared Keys are expected to be in the following format:
|
27
|
+
*
|
28
|
+
* ```
|
29
|
+
* /key/swarm/psk/1.0.0/
|
30
|
+
* /base16/
|
31
|
+
* dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d
|
32
|
+
* ```
|
33
|
+
*
|
34
|
+
* ## PSK Generation
|
35
|
+
*
|
36
|
+
* A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key.
|
37
|
+
*
|
38
|
+
* ### From a module using libp2p
|
39
|
+
*
|
40
|
+
* If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node_modules are installed.
|
41
|
+
*
|
42
|
+
* ```console
|
43
|
+
* node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key
|
44
|
+
* ```
|
45
|
+
*
|
46
|
+
* ### Programmatically
|
47
|
+
*
|
48
|
+
* ```js
|
49
|
+
* import fs from 'fs'
|
50
|
+
* import { generateKey } from '@libp2p/pnet'
|
51
|
+
*
|
52
|
+
* const swarmKey = new Uint8Array(95)
|
53
|
+
* generateKey(swarmKey)
|
54
|
+
*
|
55
|
+
* fs.writeFileSync('swarm.key', swarmKey)
|
56
|
+
* ```
|
57
|
+
*/
|
58
|
+
|
59
|
+
import { randomBytes } from '@libp2p/crypto'
|
60
|
+
import { CodeError } from '@libp2p/interface'
|
61
|
+
import { byteStream } from 'it-byte-stream'
|
62
|
+
import map from 'it-map'
|
63
|
+
import { duplexPair } from 'it-pair/duplex'
|
64
|
+
import { pipe } from 'it-pipe'
|
65
|
+
import {
|
66
|
+
createBoxStream,
|
67
|
+
createUnboxStream,
|
68
|
+
decodeV1PSK
|
69
|
+
} from './crypto.js'
|
70
|
+
import * as Errors from './errors.js'
|
71
|
+
import { NONCE_LENGTH } from './key-generator.js'
|
72
|
+
import type { ComponentLogger, Logger, ConnectionProtector, MultiaddrConnection } from '@libp2p/interface'
|
73
|
+
import type { Uint8ArrayList } from 'uint8arraylist'
|
74
|
+
|
75
|
+
export { generateKey } from './key-generator.js'
|
76
|
+
|
77
|
+
export interface ProtectorInit {
|
78
|
+
/**
|
79
|
+
* A pre-shared key. This must be the same byte value for all peers in the
|
80
|
+
* swarm in order for them to communicate.
|
81
|
+
*/
|
82
|
+
psk: Uint8Array
|
83
|
+
/**
|
84
|
+
* The initial nonce exchange must complete within this many milliseconds
|
85
|
+
* (default: 1000)
|
86
|
+
*/
|
87
|
+
timeout?: number
|
88
|
+
}
|
89
|
+
|
90
|
+
export interface ProtectorComponents {
|
91
|
+
logger: ComponentLogger
|
92
|
+
}
|
93
|
+
|
94
|
+
class PreSharedKeyConnectionProtector implements ConnectionProtector {
|
95
|
+
public tag: string
|
96
|
+
private readonly log: Logger
|
97
|
+
private readonly psk: Uint8Array
|
98
|
+
private readonly timeout: number
|
99
|
+
|
100
|
+
/**
|
101
|
+
* Takes a Private Shared Key (psk) and provides a `protect` method
|
102
|
+
* for wrapping existing connections in a private encryption stream.
|
103
|
+
*/
|
104
|
+
constructor (components: ProtectorComponents, init: ProtectorInit) {
|
105
|
+
this.log = components.logger.forComponent('libp2p:pnet')
|
106
|
+
this.timeout = init.timeout ?? 1000
|
107
|
+
|
108
|
+
const decodedPSK = decodeV1PSK(init.psk)
|
109
|
+
this.psk = decodedPSK.psk
|
110
|
+
this.tag = decodedPSK.tag ?? ''
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* Takes a given Connection and creates a private encryption stream
|
115
|
+
* between its two peers from the PSK the Protector instance was
|
116
|
+
* created with.
|
117
|
+
*/
|
118
|
+
async protect (connection: MultiaddrConnection): Promise<MultiaddrConnection> {
|
119
|
+
if (connection == null) {
|
120
|
+
throw new CodeError(Errors.NO_HANDSHAKE_CONNECTION, Errors.ERR_INVALID_PARAMETERS)
|
121
|
+
}
|
122
|
+
|
123
|
+
// Exchange nonces
|
124
|
+
this.log('protecting the connection')
|
125
|
+
const localNonce = randomBytes(NONCE_LENGTH)
|
126
|
+
|
127
|
+
const signal = AbortSignal.timeout(this.timeout)
|
128
|
+
|
129
|
+
const bytes = byteStream(connection)
|
130
|
+
|
131
|
+
const [
|
132
|
+
, result
|
133
|
+
] = await Promise.all([
|
134
|
+
bytes.write(localNonce, {
|
135
|
+
signal
|
136
|
+
}),
|
137
|
+
bytes.read(NONCE_LENGTH, {
|
138
|
+
signal
|
139
|
+
})
|
140
|
+
])
|
141
|
+
|
142
|
+
const remoteNonce = result.subarray()
|
143
|
+
|
144
|
+
// Create the boxing/unboxing pipe
|
145
|
+
this.log('exchanged nonces')
|
146
|
+
const [internal, external] = duplexPair<Uint8Array | Uint8ArrayList>()
|
147
|
+
pipe(
|
148
|
+
external,
|
149
|
+
// Encrypt all outbound traffic
|
150
|
+
createBoxStream(localNonce, this.psk),
|
151
|
+
bytes.unwrap(),
|
152
|
+
(source) => map(source, (buf) => buf.subarray()),
|
153
|
+
// Decrypt all inbound traffic
|
154
|
+
createUnboxStream(remoteNonce, this.psk),
|
155
|
+
external
|
156
|
+
).catch(this.log.error)
|
157
|
+
|
158
|
+
return {
|
159
|
+
...connection,
|
160
|
+
...internal
|
161
|
+
}
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
export function preSharedKey (init: ProtectorInit): (components: ProtectorComponents) => ConnectionProtector {
|
166
|
+
return (components) => new PreSharedKeyConnectionProtector(components, init)
|
167
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { randomBytes } from '@libp2p/crypto'
|
2
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
3
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Generates a PSK that can be used in a libp2p-pnet private network
|
7
|
+
*
|
8
|
+
* @param {Uint8Array | NodeJS.WriteStream} bytes - An object to write the psk into
|
9
|
+
* @returns {void}
|
10
|
+
*/
|
11
|
+
export function generateKey (bytes: Uint8Array | NodeJS.WriteStream): void {
|
12
|
+
const psk = uint8ArrayToString(randomBytes(KEY_LENGTH), 'base16')
|
13
|
+
const key = uint8ArrayFromString('/key/swarm/psk/1.0.0/\n/base16/\n' + psk)
|
14
|
+
|
15
|
+
if (bytes instanceof Uint8Array) {
|
16
|
+
bytes.set(key)
|
17
|
+
} else {
|
18
|
+
bytes.write(key)
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
export const NONCE_LENGTH = 24
|
23
|
+
export const KEY_LENGTH = 32
|