Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: close streams gracefully #1868

Closed
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ jobs:
path: |
package-lock.json
interop/package-lock.json
- uses: libp2p/test-plans/.github/actions/run-interop-ping-test@master
- uses: libp2p/test-plans/.github/actions/run-interop-ping-test@chore/debug-nim
with:
test-filter: js-libp2p-head
extra-versions: ${{ github.workspace }}/interop/node-version.json ${{ github.workspace }}/interop/chromium-version.json ${{ github.workspace }}/interop/firefox-version.json
Expand Down
2 changes: 1 addition & 1 deletion doc/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const node = await createLibp2p({
To define component metrics first get a reference to the metrics object:

```ts
import type { Metrics } from '@libp2p/interface-metrics'
import type { Metrics } from '@libp2p/interface/metrics'

interface MyClassComponents {
metrics: Metrics
Expand Down
2 changes: 2 additions & 0 deletions interop/BrowserDockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ WORKDIR /app/interop
RUN npx playwright install
ARG BROWSER=chromium # Options: chromium, firefox, webkit
ENV BROWSER=$BROWSER
# disable colored output and CLI animation from test runners
ENV CI true

ENTRYPOINT npm run test:interop:multidim -- --build false --types false -t browser -- --browser $BROWSER
3 changes: 3 additions & 0 deletions interop/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ COPY ./interop ./interop

WORKDIR /app/interop

# disable colored output and CLI animation from test runners
ENV CI true

ENTRYPOINT [ "npm", "run", "test:interop:multidim", "--", "--build", "false", "--types", "false", "-t", "node" ]
2 changes: 1 addition & 1 deletion interop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"scripts": {
"start": "node index.js",
"build": "aegir build",
"test:interop:multidim": "aegir test"
"test:interop:multidim": "DEBUG=libp2p*,*:trace,-libp2p:peer-store:* aegir test"
},
"dependencies": {
"@chainsafe/libp2p-noise": "^12.0.1",
Expand Down
13 changes: 11 additions & 2 deletions interop/test/ping.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,21 @@ describe('ping test', () => {

const options: Libp2pOptions<{ ping: PingService, identify: DefaultIdentifyService }> = {
start: true,
connectionManager: {
minConnections: 0
},
connectionGater: {
denyDialMultiaddr: async () => false
},
services: {
ping: pingService(),
identify: identifyService()
ping: pingService({
// increase default timeout because slow CI is slow
timeout: 60000
}),
identify: identifyService({
// increase default timeout because slow CI is slow
timeout: 60000
})
}
}

Expand Down
99 changes: 99 additions & 0 deletions packages/connection-encryption-noise/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"name": "@chainsafe/libp2p-noise",
"version": "12.0.1",
"author": "ChainSafe <info@chainsafe.io>",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ChainSafe/js-libp2p-noise#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ChainSafe/js-libp2p-noise.git"
},
"bugs": {
"url": "https://github.com/ChainSafe/js-libp2p-noise/issues"
},
"keywords": [
"crypto",
"libp2p",
"noise"
],
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"type": "module",
"types": "./dist/src/index.d.ts",
"files": [
"src",
"dist",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/strict-boolean-expressions": "off"
},
"ignorePatterns": [
"src/proto/payload.js",
"src/proto/payload.d.ts",
"test/fixtures/node-globals.js"
]
},
"scripts": {
"bench": "node benchmarks/benchmark.js",
"clean": "aegir clean",
"dep-check": "aegir dep-check",
"build": "aegir build",
"lint": "aegir lint",
"lint:fix": "aegir lint --fix",
"test": "aegir test",
"test:node": "aegir test -t node",
"test:browser": "aegir test -t browser -t webworker",
"test:electron-main": "aegir test -t electron-main",
"docs": "aegir docs",
"proto:gen": "protons ./src/proto/payload.proto",
"prepublish": "npm run build"
},
"dependencies": {
"@libp2p/crypto": "^1.0.11",
"@libp2p/interface": "~0.0.1",
"@libp2p/logger": "^2.0.5",
"@libp2p/peer-id": "^2.0.0",
"@stablelib/chacha20poly1305": "^1.0.1",
"@noble/hashes": "^1.3.0",
"@stablelib/x25519": "^1.0.3",
"it-length-prefixed": "^9.0.1",
"it-length-prefixed-stream": "^1.0.0",
"it-byte-stream": "^1.0.0",
"it-pair": "^2.0.2",
"it-pipe": "^3.0.1",
"it-stream-types": "^2.0.1",
"protons-runtime": "^5.0.0",
"uint8arraylist": "^2.3.2",
"uint8arrays": "^4.0.2"
},
"devDependencies": {
"@libp2p/interface-compliance-tests": "^3.0.0",
"@libp2p/peer-id-factory": "^2.0.0",
"@types/sinon": "^10.0.14",
"aegir": "^39.0.5",
"iso-random-stream": "^2.0.2",
"protons": "^7.0.0",
"sinon": "^15.0.0"
},
"browser": {
"./dist/src/alloc-unsafe.js": "./dist/src/alloc-unsafe-browser.js",
"util": false
}
}
5 changes: 5 additions & 0 deletions packages/connection-encryption-noise/src/@types/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type bytes = Uint8Array
export type bytes32 = Uint8Array
export type bytes16 = Uint8Array

export type uint64 = number
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { bytes } from './basic.js'
import type { NoiseSession } from './handshake.js'
import type { NoiseExtensions } from '../proto/payload.js'
import type { PeerId } from '@libp2p/interface/peer-id'

export interface IHandshake {
session: NoiseSession
remotePeer: PeerId
remoteExtensions: NoiseExtensions
encrypt: (plaintext: bytes, session: NoiseSession) => bytes
decrypt: (ciphertext: bytes, session: NoiseSession, dst?: Uint8Array) => { plaintext: bytes, valid: boolean }
}
48 changes: 48 additions & 0 deletions packages/connection-encryption-noise/src/@types/handshake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { bytes, bytes32, uint64 } from './basic.js'
import type { KeyPair } from './libp2p.js'
import type { Nonce } from '../nonce.js'

export type Hkdf = [bytes, bytes, bytes]

export interface MessageBuffer {
ne: bytes32
ns: bytes
ciphertext: bytes
}

export interface CipherState {
k: bytes32
// For performance reasons, the nonce is represented as a Nonce object
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce
}

export interface SymmetricState {
cs: CipherState
ck: bytes32 // chaining key
h: bytes32 // handshake hash
}

export interface HandshakeState {
ss: SymmetricState
s: KeyPair
e?: KeyPair
rs: bytes32
re: bytes32
psk: bytes32
}

export interface NoiseSession {
hs: HandshakeState
h?: bytes32
cs1?: CipherState
cs2?: CipherState
mc: uint64
i: boolean
}

export interface INoisePayload {
identityKey: bytes
identitySig: bytes
data: bytes
}
10 changes: 10 additions & 0 deletions packages/connection-encryption-noise/src/@types/libp2p.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { bytes32 } from './basic.js'
import type { NoiseExtensions } from '../proto/payload.js'
import type { ConnectionEncrypter } from '@libp2p/interface/connection-encrypter'

export interface KeyPair {
publicKey: bytes32
privateKey: bytes32
}

export interface INoiseConnection extends ConnectionEncrypter<NoiseExtensions> {}
4 changes: 4 additions & 0 deletions packages/connection-encryption-noise/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const NOISE_MSG_MAX_LENGTH_BYTES = 65535
export const NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG = NOISE_MSG_MAX_LENGTH_BYTES - 16

export const DUMP_SESSION_KEYS = Boolean(globalThis.process?.env?.DUMP_SESSION_KEYS)
16 changes: 16 additions & 0 deletions packages/connection-encryption-noise/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { bytes32, bytes } from './@types/basic.js'
import type { Hkdf } from './@types/handshake.js'
import type { KeyPair } from './@types/libp2p.js'

export interface ICryptoInterface {
hashSHA256: (data: Uint8Array) => Uint8Array

getHKDF: (ck: bytes32, ikm: Uint8Array) => Hkdf

generateX25519KeyPair: () => KeyPair
generateX25519KeyPairFromSeed: (seed: Uint8Array) => KeyPair
generateX25519SharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array

chaCha20Poly1305Encrypt: (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32) => bytes
chaCha20Poly1305Decrypt: (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array) => bytes | null
}
58 changes: 58 additions & 0 deletions packages/connection-encryption-noise/src/crypto/js.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { hkdf } from '@noble/hashes/hkdf'
import { sha256 } from '@noble/hashes/sha256'
import { ChaCha20Poly1305 } from '@stablelib/chacha20poly1305'
import * as x25519 from '@stablelib/x25519'
import type { bytes, bytes32 } from '../@types/basic.js'
import type { Hkdf } from '../@types/handshake.js'
import type { KeyPair } from '../@types/libp2p.js'
import type { ICryptoInterface } from '../crypto.js'

export const pureJsCrypto: ICryptoInterface = {
hashSHA256 (data: Uint8Array): Uint8Array {
return sha256(data)
},

getHKDF (ck: bytes32, ikm: Uint8Array): Hkdf {
const okm = hkdf(sha256, ikm, ck, undefined, 96)

const k1 = okm.subarray(0, 32)
const k2 = okm.subarray(32, 64)
const k3 = okm.subarray(64, 96)

return [k1, k2, k3]
},

generateX25519KeyPair (): KeyPair {
const keypair = x25519.generateKeyPair()

return {
publicKey: keypair.publicKey,
privateKey: keypair.secretKey
}
},

generateX25519KeyPairFromSeed (seed: Uint8Array): KeyPair {
const keypair = x25519.generateKeyPairFromSeed(seed)

return {
publicKey: keypair.publicKey,
privateKey: keypair.secretKey
}
},

generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
return x25519.sharedKey(privateKey, publicKey)
},

chaCha20Poly1305Encrypt (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
const ctx = new ChaCha20Poly1305(k)

return ctx.seal(nonce, plaintext, ad)
},

chaCha20Poly1305Decrypt (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array): bytes | null {
const ctx = new ChaCha20Poly1305(k)

return ctx.open(nonce, ciphertext, ad, dst)
}
}
58 changes: 58 additions & 0 deletions packages/connection-encryption-noise/src/crypto/streaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { TAG_LENGTH } from '@stablelib/chacha20poly1305'
import { NOISE_MSG_MAX_LENGTH_BYTES, NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG } from '../constants.js'
import { uint16BEEncode } from '../encoder.js'
import type { IHandshake } from '../@types/handshake-interface.js'
import type { MetricsRegistry } from '../metrics.js'
import type { Transform } from 'it-stream-types'
import type { Uint8ArrayList } from 'uint8arraylist'

// Returns generator that encrypts payload from the user
export function encryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform<AsyncIterable<Uint8Array>> {
return async function * (source) {
for await (const chunk of source) {
for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG) {
let end = i + NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG
if (end > chunk.length) {
end = chunk.length
}

const data = handshake.encrypt(chunk.subarray(i, end), handshake.session)
metrics?.encryptedPackets.increment()

yield uint16BEEncode(data.byteLength)
yield data
}
}
}
}

// Decrypt received payload to the user
export function decryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform<AsyncIterable<Uint8ArrayList>, AsyncIterable<Uint8Array>> {
return async function * (source) {
for await (const chunk of source) {
for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES) {
let end = i + NOISE_MSG_MAX_LENGTH_BYTES
if (end > chunk.length) {
end = chunk.length
}

if (end - TAG_LENGTH < i) {
throw new Error('Invalid chunk')
}
const encrypted = chunk.subarray(i, end)
// memory allocation is not cheap so reuse the encrypted Uint8Array
// see https://github.com/ChainSafe/js-libp2p-noise/pull/242#issue-1422126164
// this is ok because chacha20 reads bytes one by one and don't reread after that
// it's also tested in https://github.com/ChainSafe/as-chacha20poly1305/pull/1/files#diff-25252846b58979dcaf4e41d47b3eadd7e4f335e7fb98da6c049b1f9cd011f381R48
const dst = chunk.subarray(i, end - TAG_LENGTH)
const { plaintext: decrypted, valid } = handshake.decrypt(encrypted, handshake.session, dst)
if (!valid) {
metrics?.decryptErrors.increment()
throw new Error('Failed to validate decrypted chunk')
}
metrics?.decryptedPackets.increment()
yield decrypted
}
}
}
}
Loading