diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index ed9c8d3..a38d79e 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -22,13 +22,13 @@ const bench = async function () { console.log('Init complete, running benchmark') const bench = new benchmark('handshake', { defer: true, - fn: async function (deffered) { + fn: async function (deferred) { const [inboundConnection, outboundConnection] = duplexPair() await Promise.all([ initiator.secureOutbound(initiatorPeer, outboundConnection, responderPeer), responder.secureInbound(responderPeer, inboundConnection, initiatorPeer) ]) - deffered.resolve() + deferred.resolve() } }) .on('complete', function (stats) { diff --git a/package.json b/package.json index 701be79..bba451e 100644 --- a/package.json +++ b/package.json @@ -69,10 +69,9 @@ "dependencies": { "@chainsafe/as-chacha20poly1305": "^0.1.0", "@chainsafe/as-sha256": "^0.4.1", - "@libp2p/crypto": "^2.0.0", - "@libp2p/interface": "^0.1.0", - "@libp2p/logger": "^3.0.0", - "@libp2p/peer-id": "^3.0.0", + "@libp2p/crypto": "^3.0.0", + "@libp2p/interface": "^1.0.0", + "@libp2p/peer-id": "^4.0.0", "@noble/ciphers": "^0.4.0", "@noble/curves": "^1.1.0", "@noble/hashes": "^1.3.1", @@ -88,14 +87,14 @@ "wherearewe": "^2.0.1" }, "devDependencies": { - "@chainsafe/libp2p-yamux": "^5.0.0", + "@chainsafe/libp2p-yamux": "^6.0.0", "@libp2p/daemon-client": "^7.0.0", "@libp2p/daemon-server": "^6.0.0", - "@libp2p/interface-compliance-tests": "^4.0.0", - "@libp2p/interface-peer-id": "^2.0.2", + "@libp2p/interface-compliance-tests": "^5.0.0", "@libp2p/interop": "^9.0.0", - "@libp2p/peer-id-factory": "^3.0.0", - "@libp2p/tcp": "^8.0.0", + "@libp2p/logger": "^4.0.0", + "@libp2p/peer-id-factory": "^3.0.9", + "@libp2p/tcp": "^9.0.0", "@multiformats/multiaddr": "^12.1.0", "@types/sinon": "^17.0.1", "aegir": "^41.1.10", @@ -103,7 +102,7 @@ "execa": "^8.0.1", "go-libp2p": "^1.0.3", "iso-random-stream": "^2.0.2", - "libp2p": "^0.46.0", + "libp2p": "next", "mkdirp": "^3.0.0", "p-defer": "^4.0.0", "protons": "^7.0.0", diff --git a/src/@types/handshake-interface.ts b/src/@types/handshake-interface.ts index 7213a52..a02af2a 100644 --- a/src/@types/handshake-interface.ts +++ b/src/@types/handshake-interface.ts @@ -1,12 +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' +import type { PeerId } from '@libp2p/interface' +import type { Uint8ArrayList } from 'uint8arraylist' 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 } + encrypt(plaintext: Uint8Array | Uint8ArrayList, session: NoiseSession): Uint8Array | Uint8ArrayList + decrypt(ciphertext: Uint8Array | Uint8ArrayList, session: NoiseSession, dst?: Uint8Array): { plaintext: Uint8Array | Uint8ArrayList, valid: boolean } } diff --git a/src/@types/handshake.ts b/src/@types/handshake.ts index ec333b7..46754ea 100644 --- a/src/@types/handshake.ts +++ b/src/@types/handshake.ts @@ -1,13 +1,14 @@ import type { bytes, bytes32, uint64 } from './basic.js' import type { KeyPair } from './libp2p.js' import type { Nonce } from '../nonce.js' +import type { Uint8ArrayList } from 'uint8arraylist' export type Hkdf = [bytes, bytes, bytes] export interface MessageBuffer { ne: bytes32 - ns: bytes - ciphertext: bytes + ns: Uint8Array | Uint8ArrayList + ciphertext: Uint8Array | Uint8ArrayList } export interface CipherState { @@ -27,7 +28,7 @@ export interface HandshakeState { ss: SymmetricState s: KeyPair e?: KeyPair - rs: bytes32 + rs: Uint8Array | Uint8ArrayList re: bytes32 psk: bytes32 } diff --git a/src/@types/libp2p.ts b/src/@types/libp2p.ts index c20fe93..055b753 100644 --- a/src/@types/libp2p.ts +++ b/src/@types/libp2p.ts @@ -1,6 +1,6 @@ import type { bytes32 } from './basic.js' import type { NoiseExtensions } from '../proto/payload.js' -import type { ConnectionEncrypter } from '@libp2p/interface/connection-encrypter' +import type { ConnectionEncrypter } from '@libp2p/interface' export interface KeyPair { publicKey: bytes32 diff --git a/src/crypto.ts b/src/crypto.ts index bbbbfff..cfb048c 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,16 +1,17 @@ -import type { bytes32, bytes } from './@types/basic.js' +import { type Uint8ArrayList } from 'uint8arraylist' +import type { bytes32 } 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 + hashSHA256(data: Uint8Array | Uint8ArrayList): Uint8Array getHKDF(ck: bytes32, ikm: Uint8Array): Hkdf generateX25519KeyPair(): KeyPair generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair - generateX25519SharedKey(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array + generateX25519SharedKey(privateKey: Uint8Array | Uint8ArrayList, publicKey: Uint8Array | Uint8ArrayList): Uint8Array - chaCha20Poly1305Encrypt(plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes - chaCha20Poly1305Decrypt(ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array): bytes | null + chaCha20Poly1305Encrypt(plaintext: Uint8Array | Uint8ArrayList, nonce: Uint8Array, ad: Uint8Array, k: bytes32): Uint8ArrayList | Uint8Array + chaCha20Poly1305Decrypt(ciphertext: Uint8Array | Uint8ArrayList, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array): Uint8ArrayList | Uint8Array | null } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 946799c..3b57d98 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1,6 +1,7 @@ import crypto from 'node:crypto' import { newInstance, ChaCha20Poly1305 } from '@chainsafe/as-chacha20poly1305' import { digest } from '@chainsafe/as-sha256' +import { Uint8ArrayList } from 'uint8arraylist' import { isElectronMain } from 'wherearewe' import { pureJsCrypto } from './js.js' import type { KeyPair } from '../@types/libp2p.js' @@ -13,7 +14,17 @@ const PKCS8_PREFIX = Buffer.from([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06 const X25519_PREFIX = Buffer.from([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00]) const nodeCrypto: Pick = { hashSHA256 (data) { - return crypto.createHash('sha256').update(data).digest() + const hash = crypto.createHash('sha256') + + if (data instanceof Uint8Array) { + return hash.update(data).digest() + } + + for (const buf of data) { + hash.update(buf) + } + + return hash.digest() }, chaCha20Poly1305Encrypt (plaintext, nonce, ad, k) { @@ -21,42 +32,87 @@ const nodeCrypto: Pick 0) { + output.append(final) + } + + output.append(cipher.getAuthTag()) + + return output }, chaCha20Poly1305Decrypt (ciphertext, nonce, ad, k, _dst) { const authTag = ciphertext.subarray(ciphertext.length - 16) - const text = ciphertext.subarray(0, ciphertext.length - 16) const decipher = crypto.createDecipheriv(CHACHA_POLY1305, k, nonce, { authTagLength: 16 }) + + let text: Uint8Array | Uint8ArrayList + + if (ciphertext instanceof Uint8Array) { + text = ciphertext.subarray(0, ciphertext.length - 16) + } else { + text = ciphertext.sublist(0, ciphertext.length - 16) + } + decipher.setAAD(ad, { plaintextLength: text.byteLength }) decipher.setAuthTag(authTag) - const updated = decipher.update(text) + + if (text instanceof Uint8Array) { + const output = decipher.update(text) + const final = decipher.final() + + if (final.byteLength > 0) { + return Buffer.concat([output, final], output.byteLength + final.byteLength) + } + + return output + } + + const output = new Uint8ArrayList() + + for (const buf of text) { + output.append(decipher.update(buf)) + } + const final = decipher.final() + if (final.byteLength > 0) { - return Buffer.concat([updated, final], updated.byteLength + final.byteLength) + output.append(final) } - return updated + + return output } } const asCrypto: Pick = { hashSHA256 (data) { - return digest(data) + return digest(data.subarray()) }, chaCha20Poly1305Encrypt (plaintext, nonce, ad, k) { - return asImpl.seal(k, nonce, plaintext, ad) + return asImpl.seal(k, nonce, plaintext.subarray(), ad) }, chaCha20Poly1305Decrypt (ciphertext, nonce, ad, k, dst) { - return asImpl.open(k, nonce, ciphertext, ad, dst) + return asImpl.open(k, nonce, ciphertext.subarray(), ad, dst) } } @@ -69,13 +125,13 @@ export const defaultCrypto: ICryptoInterface = { return nodeCrypto.hashSHA256(data) }, chaCha20Poly1305Encrypt (plaintext, nonce, ad, k) { - if (plaintext.length < 1200) { + if (plaintext.byteLength < 1200) { return asCrypto.chaCha20Poly1305Encrypt(plaintext, nonce, ad, k) } return nodeCrypto.chaCha20Poly1305Encrypt(plaintext, nonce, ad, k) }, chaCha20Poly1305Decrypt (ciphertext, nonce, ad, k, dst) { - if (ciphertext.length < 1200) { + if (ciphertext.byteLength < 1200) { return asCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst) } return nodeCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst) @@ -118,16 +174,26 @@ export const defaultCrypto: ICryptoInterface = { privateKey: seed } }, - generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array { - publicKey = Buffer.concat([ - X25519_PREFIX, - publicKey - ], X25519_PREFIX.byteLength + publicKey.byteLength) - - privateKey = Buffer.concat([ - PKCS8_PREFIX, - privateKey - ], PKCS8_PREFIX.byteLength + privateKey.byteLength) + generateX25519SharedKey (privateKey: Uint8Array | Uint8ArrayList, publicKey: Uint8Array | Uint8ArrayList): Uint8Array { + if (publicKey instanceof Uint8Array) { + publicKey = Buffer.concat([ + X25519_PREFIX, + publicKey + ], X25519_PREFIX.byteLength + publicKey.byteLength) + } else { + publicKey.prepend(X25519_PREFIX) + publicKey = publicKey.subarray() + } + + if (privateKey instanceof Uint8Array) { + privateKey = Buffer.concat([ + PKCS8_PREFIX, + privateKey + ], PKCS8_PREFIX.byteLength + privateKey.byteLength) + } else { + privateKey.prepend(PKCS8_PREFIX) + privateKey = privateKey.subarray() + } return crypto.diffieHellman({ publicKey: crypto.createPublicKey({ diff --git a/src/crypto/js.ts b/src/crypto/js.ts index e47d476..034a0a3 100644 --- a/src/crypto/js.ts +++ b/src/crypto/js.ts @@ -2,14 +2,15 @@ import { chacha20poly1305 } from '@noble/ciphers/chacha' import { x25519 } from '@noble/curves/ed25519' import { extract, expand } from '@noble/hashes/hkdf' import { sha256 } from '@noble/hashes/sha256' -import type { bytes, bytes32 } from '../@types/basic.js' +import type { 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' +import type { Uint8ArrayList } from 'uint8arraylist' export const pureJsCrypto: ICryptoInterface = { - hashSHA256 (data: Uint8Array): Uint8Array { - return sha256(data) + hashSHA256 (data: Uint8Array | Uint8ArrayList): Uint8Array { + return sha256(data.subarray()) }, getHKDF (ck: bytes32, ikm: Uint8Array): Hkdf { @@ -43,15 +44,15 @@ export const pureJsCrypto: ICryptoInterface = { } }, - generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array { - return x25519.getSharedSecret(privateKey, publicKey) + generateX25519SharedKey (privateKey: Uint8Array | Uint8ArrayList, publicKey: Uint8Array | Uint8ArrayList): Uint8Array { + return x25519.getSharedSecret(privateKey.subarray(), publicKey.subarray()) }, - chaCha20Poly1305Encrypt (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes { - return chacha20poly1305(k, nonce, ad).encrypt(plaintext) + chaCha20Poly1305Encrypt (plaintext: Uint8Array | Uint8ArrayList, nonce: Uint8Array, ad: Uint8Array, k: bytes32): Uint8Array { + return chacha20poly1305(k, nonce, ad).encrypt(plaintext.subarray()) }, - chaCha20Poly1305Decrypt (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array): bytes | null { - return chacha20poly1305(k, nonce, ad).decrypt(ciphertext, dst) + chaCha20Poly1305Decrypt (ciphertext: Uint8Array | Uint8ArrayList, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array): Uint8Array | null { + return chacha20poly1305(k, nonce, ad).decrypt(ciphertext.subarray(), dst) } } diff --git a/src/crypto/streaming.ts b/src/crypto/streaming.ts index c5cc85e..a9d56fb 100644 --- a/src/crypto/streaming.ts +++ b/src/crypto/streaming.ts @@ -1,15 +1,14 @@ -import { concat as uint8ArrayConcat } from 'uint8arrays' +import { Uint8ArrayList } from 'uint8arraylist' 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' const CHACHA_TAG_LENGTH = 16 // Returns generator that encrypts payload from the user -export function encryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform> { +export function encryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform> { 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) { @@ -18,20 +17,24 @@ export function encryptStream (handshake: IHandshake, metrics?: MetricsRegistry) end = chunk.length } - const data = handshake.encrypt(chunk.subarray(i, end), handshake.session) + let data: Uint8Array | Uint8ArrayList + + if (chunk instanceof Uint8Array) { + data = handshake.encrypt(chunk.subarray(i, end), handshake.session) + } else { + data = handshake.encrypt(chunk.sublist(i, end), handshake.session) + } + metrics?.encryptedPackets.increment() - yield uint8ArrayConcat([ - uint16BEEncode(data.byteLength), - data - ], 2 + data.byteLength) + yield new Uint8ArrayList(uint16BEEncode(data.byteLength), data) } } } } // Decrypt received payload to the user -export function decryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform, AsyncIterable> { +export function decryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform, AsyncGenerator> { return async function * (source) { for await (const chunk of source) { for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES) { @@ -43,7 +46,8 @@ export function decryptStream (handshake: IHandshake, metrics?: MetricsRegistry) if (end - CHACHA_TAG_LENGTH < i) { throw new Error('Invalid chunk') } - const encrypted = chunk.subarray(i, end) + + const encrypted = chunk.sublist(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 diff --git a/src/encoder.ts b/src/encoder.ts index c149b4c..8c4d203 100644 --- a/src/encoder.ts +++ b/src/encoder.ts @@ -1,9 +1,8 @@ +import { Uint8ArrayList } from 'uint8arraylist' import { alloc as uint8ArrayAlloc, allocUnsafe as uint8ArrayAllocUnsafe } from 'uint8arrays/alloc' -import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import type { bytes } from './@types/basic.js' import type { MessageBuffer } from './@types/handshake.js' import type { LengthDecoderFunction } from 'it-length-prefixed' -import type { Uint8ArrayList } from 'uint8arraylist' export const uint16BEEncode = (value: number): Uint8Array => { const target = uint8ArrayAllocUnsafe(2) @@ -25,16 +24,16 @@ uint16BEDecode.bytes = 2 // Note: IK and XX encoder usage is opposite (XX uses in stages encode0 where IK uses encode1) -export function encode0 (message: MessageBuffer): bytes { - return uint8ArrayConcat([message.ne, message.ciphertext], message.ne.length + message.ciphertext.length) +export function encode0 (message: MessageBuffer): Uint8ArrayList { + return new Uint8ArrayList(message.ne, message.ciphertext) } -export function encode1 (message: MessageBuffer): bytes { - return uint8ArrayConcat([message.ne, message.ns, message.ciphertext], message.ne.length + message.ns.length + message.ciphertext.length) +export function encode1 (message: MessageBuffer): Uint8ArrayList { + return new Uint8ArrayList(message.ne, message.ns, message.ciphertext) } -export function encode2 (message: MessageBuffer): bytes { - return uint8ArrayConcat([message.ns, message.ciphertext], message.ns.length + message.ciphertext.length) +export function encode2 (message: MessageBuffer): Uint8ArrayList { + return new Uint8ArrayList(message.ns, message.ciphertext) } export function decode0 (input: bytes): MessageBuffer { diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..2e1aa43 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,21 @@ +export class UnexpectedPeerError extends Error { + public code: string + + constructor (message = 'Unexpected Peer') { + super(message) + this.code = UnexpectedPeerError.code + } + + static readonly code = 'ERR_UNEXPECTED_PEER' +} + +export class InvalidCryptoExchangeError extends Error { + public code: string + + constructor (message = 'Invalid crypto exchange') { + super(message) + this.code = InvalidCryptoExchangeError.code + } + + static readonly code = 'ERR_INVALID_CRYPTO_EXCHANGE' +} diff --git a/src/handshake-xx.ts b/src/handshake-xx.ts index 3b547dc..a64c518 100644 --- a/src/handshake-xx.ts +++ b/src/handshake-xx.ts @@ -1,9 +1,8 @@ -import { InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface/errors' import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc' import { decode0, decode1, decode2, encode0, encode1, encode2 } from './encoder.js' +import { InvalidCryptoExchangeError, UnexpectedPeerError } from './errors.js' import { XX } from './handshakes/xx.js' import { - logger, logLocalStaticKeys, logLocalEphemeralKeys, logRemoteEphemeralKey, @@ -20,9 +19,11 @@ import type { IHandshake } from './@types/handshake-interface.js' import type { CipherState, NoiseSession } from './@types/handshake.js' import type { KeyPair } from './@types/libp2p.js' import type { ICryptoInterface } from './crypto.js' +import type { NoiseComponents } from './index.js' import type { NoiseExtensions } from './proto/payload.js' -import type { PeerId } from '@libp2p/interface/peer-id' +import type { Logger, PeerId } from '@libp2p/interface' import type { LengthPrefixedStream } from 'it-length-prefixed-stream' +import type { Uint8ArrayList } from 'uint8arraylist' export class XXHandshake implements IHandshake { public isInitiator: boolean @@ -36,8 +37,10 @@ export class XXHandshake implements IHandshake { protected staticKeypair: KeyPair private readonly prologue: bytes32 + private readonly log: Logger constructor ( + components: NoiseComponents, isInitiator: boolean, payload: bytes, prologue: bytes32, @@ -47,6 +50,7 @@ export class XXHandshake implements IHandshake { remotePeer?: PeerId, handshake?: XX ) { + this.log = components.logger.forComponent('libp2p:noise:xxhandshake') this.isInitiator = isInitiator this.payload = payload this.prologue = prologue @@ -55,45 +59,45 @@ export class XXHandshake implements IHandshake { if (remotePeer) { this.remotePeer = remotePeer } - this.xx = handshake ?? new XX(crypto) + this.xx = handshake ?? new XX(components, crypto) this.session = this.xx.initSession(this.isInitiator, this.prologue, this.staticKeypair) } // stage 0 public async propose (): Promise { - logLocalStaticKeys(this.session.hs.s) + logLocalStaticKeys(this.session.hs.s, this.log) if (this.isInitiator) { - logger.trace('Stage 0 - Initiator starting to send first message.') + this.log.trace('Stage 0 - Initiator starting to send first message.') const messageBuffer = this.xx.sendMessage(this.session, uint8ArrayAlloc(0)) await this.connection.write(encode0(messageBuffer)) - logger.trace('Stage 0 - Initiator finished sending first message.') - logLocalEphemeralKeys(this.session.hs.e) + this.log.trace('Stage 0 - Initiator finished sending first message.') + logLocalEphemeralKeys(this.session.hs.e, this.log) } else { - logger.trace('Stage 0 - Responder waiting to receive first message...') + this.log.trace('Stage 0 - Responder waiting to receive first message...') const receivedMessageBuffer = decode0((await this.connection.read()).subarray()) const { valid } = this.xx.recvMessage(this.session, receivedMessageBuffer) if (!valid) { throw new InvalidCryptoExchangeError('xx handshake stage 0 validation fail') } - logger.trace('Stage 0 - Responder received first message.') - logRemoteEphemeralKey(this.session.hs.re) + this.log.trace('Stage 0 - Responder received first message.') + logRemoteEphemeralKey(this.session.hs.re, this.log) } } // stage 1 public async exchange (): Promise { if (this.isInitiator) { - logger.trace('Stage 1 - Initiator waiting to receive first message from responder...') + this.log.trace('Stage 1 - Initiator waiting to receive first message from responder...') const receivedMessageBuffer = decode1((await this.connection.read()).subarray()) const { plaintext, valid } = this.xx.recvMessage(this.session, receivedMessageBuffer) if (!valid) { throw new InvalidCryptoExchangeError('xx handshake stage 1 validation fail') } - logger.trace('Stage 1 - Initiator received the message.') - logRemoteEphemeralKey(this.session.hs.re) - logRemoteStaticKey(this.session.hs.rs) + this.log.trace('Stage 1 - Initiator received the message.') + logRemoteEphemeralKey(this.session.hs.re, this.log) + logRemoteStaticKey(this.session.hs.rs, this.log) - logger.trace("Initiator going to check remote's signature...") + this.log.trace("Initiator going to check remote's signature...") try { const decodedPayload = decodePayload(plaintext) this.remotePeer = this.remotePeer || await getPeerIdFromPayload(decodedPayload) @@ -103,31 +107,31 @@ export class XXHandshake implements IHandshake { const err = e as Error throw new UnexpectedPeerError(`Error occurred while verifying signed payload: ${err.message}`) } - logger.trace('All good with the signature!') + this.log.trace('All good with the signature!') } else { - logger.trace('Stage 1 - Responder sending out first message with signed payload and static key.') + this.log.trace('Stage 1 - Responder sending out first message with signed payload and static key.') const messageBuffer = this.xx.sendMessage(this.session, this.payload) await this.connection.write(encode1(messageBuffer)) - logger.trace('Stage 1 - Responder sent the second handshake message with signed payload.') - logLocalEphemeralKeys(this.session.hs.e) + this.log.trace('Stage 1 - Responder sent the second handshake message with signed payload.') + logLocalEphemeralKeys(this.session.hs.e, this.log) } } // stage 2 public async finish (): Promise { if (this.isInitiator) { - logger.trace('Stage 2 - Initiator sending third handshake message.') + this.log.trace('Stage 2 - Initiator sending third handshake message.') const messageBuffer = this.xx.sendMessage(this.session, this.payload) await this.connection.write(encode2(messageBuffer)) - logger.trace('Stage 2 - Initiator sent message with signed payload.') + this.log.trace('Stage 2 - Initiator sent message with signed payload.') } else { - logger.trace('Stage 2 - Responder waiting for third handshake message...') + this.log.trace('Stage 2 - Responder waiting for third handshake message...') const receivedMessageBuffer = decode2((await this.connection.read()).subarray()) const { plaintext, valid } = this.xx.recvMessage(this.session, receivedMessageBuffer) if (!valid) { throw new InvalidCryptoExchangeError('xx handshake stage 2 validation fail') } - logger.trace('Stage 2 - Responder received the message, finished handshake.') + this.log.trace('Stage 2 - Responder received the message, finished handshake.') try { const decodedPayload = decodePayload(plaintext) @@ -139,22 +143,22 @@ export class XXHandshake implements IHandshake { throw new UnexpectedPeerError(`Error occurred while verifying signed payload: ${err.message}`) } } - logCipherState(this.session) + logCipherState(this.session, this.log) } - public encrypt (plaintext: Uint8Array, session: NoiseSession): bytes { + public encrypt (plaintext: Uint8Array | Uint8ArrayList, session: NoiseSession): Uint8Array | Uint8ArrayList { const cs = this.getCS(session) return this.xx.encryptWithAd(cs, uint8ArrayAlloc(0), plaintext) } - public decrypt (ciphertext: Uint8Array, session: NoiseSession, dst?: Uint8Array): { plaintext: bytes, valid: boolean } { + public decrypt (ciphertext: Uint8Array | Uint8ArrayList, session: NoiseSession, dst?: Uint8Array): { plaintext: Uint8Array | Uint8ArrayList, valid: boolean } { const cs = this.getCS(session, false) return this.xx.decryptWithAd(cs, uint8ArrayAlloc(0), ciphertext, dst) } - public getRemoteStaticKey (): bytes { + public getRemoteStaticKey (): Uint8Array | Uint8ArrayList { return this.session.hs.rs } diff --git a/src/handshakes/abstract-handshake.ts b/src/handshakes/abstract-handshake.ts index 308d298..9e34a61 100644 --- a/src/handshakes/abstract-handshake.ts +++ b/src/handshakes/abstract-handshake.ts @@ -1,15 +1,16 @@ +import { Uint8ArrayList } from 'uint8arraylist' import { fromString as uint8ArrayFromString } from 'uint8arrays' import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc' -import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' -import { logger } from '../logger.js' import { Nonce } from '../nonce.js' import type { bytes, bytes32 } from '../@types/basic.js' import type { CipherState, MessageBuffer, SymmetricState } from '../@types/handshake.js' import type { ICryptoInterface } from '../crypto.js' +import type { NoiseComponents } from '../index.js' +import type { Logger } from '@libp2p/interface' export interface DecryptedResult { - plaintext: bytes + plaintext: Uint8ArrayList | Uint8Array valid: boolean } @@ -20,19 +21,21 @@ export interface SplitState { export abstract class AbstractHandshake { public crypto: ICryptoInterface + private readonly log: Logger - constructor (crypto: ICryptoInterface) { + constructor (components: NoiseComponents, crypto: ICryptoInterface) { + this.log = components.logger.forComponent('libp2p:noise:abstract-handshake') this.crypto = crypto } - public encryptWithAd (cs: CipherState, ad: Uint8Array, plaintext: Uint8Array): bytes { + public encryptWithAd (cs: CipherState, ad: Uint8Array, plaintext: Uint8Array | Uint8ArrayList): Uint8Array | Uint8ArrayList { const e = this.encrypt(cs.k, cs.n, ad, plaintext) cs.n.increment() return e } - public decryptWithAd (cs: CipherState, ad: Uint8Array, ciphertext: Uint8Array, dst?: Uint8Array): DecryptedResult { + public decryptWithAd (cs: CipherState, ad: Uint8Array, ciphertext: Uint8Array | Uint8ArrayList, dst?: Uint8Array): DecryptedResult { const { plaintext, valid } = this.decrypt(cs.k, cs.n, ad, ciphertext, dst) if (valid) cs.n.increment() @@ -53,13 +56,13 @@ export abstract class AbstractHandshake { return uint8ArrayEquals(emptyKey, k) } - protected encrypt (k: bytes32, n: Nonce, ad: Uint8Array, plaintext: Uint8Array): bytes { + protected encrypt (k: bytes32, n: Nonce, ad: Uint8Array, plaintext: Uint8Array | Uint8ArrayList): Uint8Array | Uint8ArrayList { n.assertValue() return this.crypto.chaCha20Poly1305Encrypt(plaintext, n.getBytes(), ad, k) } - protected encryptAndHash (ss: SymmetricState, plaintext: bytes): bytes { + protected encryptAndHash (ss: SymmetricState, plaintext: bytes): Uint8Array | Uint8ArrayList { let ciphertext if (this.hasKey(ss.cs)) { ciphertext = this.encryptWithAd(ss.cs, ss.h, plaintext) @@ -71,7 +74,7 @@ export abstract class AbstractHandshake { return ciphertext } - protected decrypt (k: bytes32, n: Nonce, ad: bytes, ciphertext: bytes, dst?: Uint8Array): DecryptedResult { + protected decrypt (k: bytes32, n: Nonce, ad: bytes, ciphertext: Uint8Array | Uint8ArrayList, dst?: Uint8Array): DecryptedResult { n.assertValue() const encryptedMessage = this.crypto.chaCha20Poly1305Decrypt(ciphertext, n.getBytes(), ad, k, dst) @@ -89,8 +92,9 @@ export abstract class AbstractHandshake { } } - protected decryptAndHash (ss: SymmetricState, ciphertext: bytes): DecryptedResult { - let plaintext: bytes; let valid = true + protected decryptAndHash (ss: SymmetricState, ciphertext: Uint8Array | Uint8ArrayList): DecryptedResult { + let plaintext: Uint8Array | Uint8ArrayList + let valid = true if (this.hasKey(ss.cs)) { ({ plaintext, valid } = this.decryptWithAd(ss.cs, ss.h, ciphertext)) } else { @@ -101,7 +105,7 @@ export abstract class AbstractHandshake { return { plaintext, valid } } - protected dh (privateKey: bytes32, publicKey: bytes32): bytes32 { + protected dh (privateKey: bytes32, publicKey: Uint8Array | Uint8ArrayList): bytes32 { try { const derivedU8 = this.crypto.generateX25519SharedKey(privateKey, publicKey) @@ -112,17 +116,17 @@ export abstract class AbstractHandshake { return derivedU8.subarray(0, 32) } catch (e) { const err = e as Error - logger.error(err) + this.log.error('error deriving shared key', err) return uint8ArrayAlloc(32) } } - protected mixHash (ss: SymmetricState, data: bytes): void { + protected mixHash (ss: SymmetricState, data: Uint8Array | Uint8ArrayList): void { ss.h = this.getHash(ss.h, data) } - protected getHash (a: Uint8Array, b: Uint8Array): bytes32 { - const u = this.crypto.hashSHA256(uint8ArrayConcat([a, b], a.length + b.length)) + protected getHash (a: Uint8Array, b: Uint8Array | Uint8ArrayList): Uint8Array { + const u = this.crypto.hashSHA256(new Uint8ArrayList(a, b)) return u } diff --git a/src/handshakes/xx.ts b/src/handshakes/xx.ts index 6107738..5a41e3b 100644 --- a/src/handshakes/xx.ts +++ b/src/handshakes/xx.ts @@ -4,6 +4,7 @@ import { AbstractHandshake, type DecryptedResult } from './abstract-handshake.js import type { bytes32, bytes } from '../@types/basic.js' import type { CipherState, HandshakeState, MessageBuffer, NoiseSession } from '../@types/handshake.js' import type { KeyPair } from '../@types/libp2p.js' +import type { Uint8ArrayList } from 'uint8arraylist' export class XX extends AbstractHandshake { private initializeInitiator (prologue: bytes32, s: KeyPair, rs: bytes32, psk: bytes32): HandshakeState { @@ -96,7 +97,7 @@ export class XX extends AbstractHandshake { return { plaintext, valid: (valid1 && valid2) } } - private readMessageC (hs: HandshakeState, message: MessageBuffer): { h: bytes, plaintext: bytes, valid: boolean, cs1: CipherState, cs2: CipherState } { + private readMessageC (hs: HandshakeState, message: MessageBuffer): { h: bytes, plaintext: Uint8Array | Uint8ArrayList, valid: boolean, cs1: CipherState, cs2: CipherState } { const { plaintext: ns, valid: valid1 } = this.decryptAndHash(hs.ss, message.ns) if (valid1 && isValidPublicKey(ns)) { hs.rs = ns @@ -165,7 +166,7 @@ export class XX extends AbstractHandshake { } public recvMessage (session: NoiseSession, message: MessageBuffer): DecryptedResult { - let plaintext: bytes = uint8ArrayAlloc(0) + let plaintext: Uint8Array | Uint8ArrayList = uint8ArrayAlloc(0) let valid = false if (session.mc === 0) { ({ plaintext, valid } = this.readMessageA(session.hs, message)) diff --git a/src/index.ts b/src/index.ts index 3a42c89..c4397f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,15 @@ import { Noise } from './noise.js' import type { NoiseInit } from './noise.js' import type { NoiseExtensions } from './proto/payload.js' -import type { ConnectionEncrypter } from '@libp2p/interface/connection-encrypter' +import type { ComponentLogger, ConnectionEncrypter, Metrics } from '@libp2p/interface' export type { ICryptoInterface } from './crypto.js' export { pureJsCrypto } from './crypto/js.js' -export function noise (init: NoiseInit = {}): () => ConnectionEncrypter { - return () => new Noise(init) +export interface NoiseComponents { + logger: ComponentLogger + metrics?: Metrics +} + +export function noise (init: NoiseInit = {}): (components: NoiseComponents) => ConnectionEncrypter { + return (components: NoiseComponents) => new Noise(components, init) } diff --git a/src/logger.ts b/src/logger.ts index b44ca7b..c57b456 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,30 +1,24 @@ -import { type Logger, logger } from '@libp2p/logger' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { DUMP_SESSION_KEYS } from './constants.js' import type { NoiseSession } from './@types/handshake.js' import type { KeyPair } from './@types/libp2p.js' +import type { Logger } from '@libp2p/interface' +import type { Uint8ArrayList } from 'uint8arraylist' -const log = logger('libp2p:noise') - -export { log as logger } - -let keyLogger: Logger -if (DUMP_SESSION_KEYS) { - keyLogger = log -} else { - keyLogger = Object.assign(() => { /* do nothing */ }, { - enabled: false, - trace: () => {}, - error: () => {} - }) -} +export function logLocalStaticKeys (s: KeyPair, keyLogger: Logger): void { + if (!keyLogger.enabled || !DUMP_SESSION_KEYS) { + return + } -export function logLocalStaticKeys (s: KeyPair): void { keyLogger(`LOCAL_STATIC_PUBLIC_KEY ${uint8ArrayToString(s.publicKey, 'hex')}`) keyLogger(`LOCAL_STATIC_PRIVATE_KEY ${uint8ArrayToString(s.privateKey, 'hex')}`) } -export function logLocalEphemeralKeys (e: KeyPair | undefined): void { +export function logLocalEphemeralKeys (e: KeyPair | undefined, keyLogger: Logger): void { + if (!keyLogger.enabled || !DUMP_SESSION_KEYS) { + return + } + if (e) { keyLogger(`LOCAL_PUBLIC_EPHEMERAL_KEY ${uint8ArrayToString(e.publicKey, 'hex')}`) keyLogger(`LOCAL_PRIVATE_EPHEMERAL_KEY ${uint8ArrayToString(e.privateKey, 'hex')}`) @@ -33,15 +27,27 @@ export function logLocalEphemeralKeys (e: KeyPair | undefined): void { } } -export function logRemoteStaticKey (rs: Uint8Array): void { - keyLogger(`REMOTE_STATIC_PUBLIC_KEY ${uint8ArrayToString(rs, 'hex')}`) +export function logRemoteStaticKey (rs: Uint8Array | Uint8ArrayList, keyLogger: Logger): void { + if (!keyLogger.enabled || !DUMP_SESSION_KEYS) { + return + } + + keyLogger(`REMOTE_STATIC_PUBLIC_KEY ${uint8ArrayToString(rs.subarray(), 'hex')}`) } -export function logRemoteEphemeralKey (re: Uint8Array): void { - keyLogger(`REMOTE_EPHEMERAL_PUBLIC_KEY ${uint8ArrayToString(re, 'hex')}`) +export function logRemoteEphemeralKey (re: Uint8Array | Uint8ArrayList, keyLogger: Logger): void { + if (!keyLogger.enabled || !DUMP_SESSION_KEYS) { + return + } + + keyLogger(`REMOTE_EPHEMERAL_PUBLIC_KEY ${uint8ArrayToString(re.subarray(), 'hex')}`) } -export function logCipherState (session: NoiseSession): void { +export function logCipherState (session: NoiseSession, keyLogger: Logger): void { + if (!keyLogger.enabled || !DUMP_SESSION_KEYS) { + return + } + if (session.cs1 && session.cs2) { keyLogger(`CIPHER_STATE_1 ${session.cs1.n.getUint64()} ${uint8ArrayToString(session.cs1.k, 'hex')}`) keyLogger(`CIPHER_STATE_2 ${session.cs2.n.getUint64()} ${uint8ArrayToString(session.cs2.k, 'hex')}`) diff --git a/src/metrics.ts b/src/metrics.ts index 8d0b3a4..3733d41 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,4 +1,4 @@ -import type { Counter, Metrics } from '@libp2p/interface/metrics' +import type { Counter, Metrics } from '@libp2p/interface' export type MetricsRegistry = Record diff --git a/src/noise.ts b/src/noise.ts index 3b52bd2..1313691 100644 --- a/src/noise.ts +++ b/src/noise.ts @@ -14,11 +14,11 @@ import type { bytes } from './@types/basic.js' import type { IHandshake } from './@types/handshake-interface.js' import type { INoiseConnection, KeyPair } from './@types/libp2p.js' import type { ICryptoInterface } from './crypto.js' +import type { NoiseComponents } from './index.js' import type { NoiseExtensions } from './proto/payload.js' -import type { SecuredConnection } from '@libp2p/interface/connection-encrypter' -import type { Metrics } from '@libp2p/interface/metrics' -import type { PeerId } from '@libp2p/interface/peer-id' -import type { Duplex, Source } from 'it-stream-types' +import type { MultiaddrConnection, SecuredConnection, PeerId } from '@libp2p/interface' +import type { Duplex } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' interface HandshakeParams { connection: LengthPrefixedStream @@ -35,7 +35,6 @@ export interface NoiseInit { extensions?: NoiseExtensions crypto?: ICryptoInterface prologueBytes?: Uint8Array - metrics?: Metrics } export class Noise implements INoiseConnection { @@ -46,10 +45,13 @@ export class Noise implements INoiseConnection { private readonly staticKeys: KeyPair private readonly extensions?: NoiseExtensions private readonly metrics?: MetricsRegistry + private readonly components: NoiseComponents - constructor (init: NoiseInit = {}) { - const { staticNoiseKey, extensions, crypto, prologueBytes, metrics } = init + constructor (components: NoiseComponents, init: NoiseInit = {}) { + const { staticNoiseKey, extensions, crypto, prologueBytes } = init + const { metrics } = components + this.components = components this.crypto = crypto ?? defaultCrypto this.extensions = extensions this.metrics = metrics ? registerMetrics(metrics) : undefined @@ -67,11 +69,11 @@ export class Noise implements INoiseConnection { * Encrypt outgoing data to the remote party (handshake as initiator) * * @param {PeerId} localPeer - PeerId of the receiving peer - * @param {Duplex, AsyncIterable, Promise>} connection - streaming iterable duplex that will be encrypted + * @param {Stream} connection - streaming iterable duplex that will be encrypted * @param {PeerId} remotePeer - PeerId of the remote peer. Used to validate the integrity of the remote peer. - * @returns {Promise} + * @returns {Promise>} */ - public async secureOutbound (localPeer: PeerId, connection: Duplex, AsyncIterable, Promise>, remotePeer?: PeerId): Promise> { + public async secureOutbound > = MultiaddrConnection> (localPeer: PeerId, connection: Stream, remotePeer?: PeerId): Promise> { const wrappedConnection = lpStream( connection, { @@ -88,8 +90,11 @@ export class Noise implements INoiseConnection { }) const conn = await this.createSecureConnection(wrappedConnection, handshake) + connection.source = conn.source + connection.sink = conn.sink + return { - conn, + conn: connection, remoteExtensions: handshake.remoteExtensions, remotePeer: handshake.remotePeer } @@ -99,11 +104,11 @@ export class Noise implements INoiseConnection { * Decrypt incoming data (handshake as responder). * * @param {PeerId} localPeer - PeerId of the receiving peer. - * @param {Duplex, AsyncIterable, Promise>} connection - streaming iterable duplex that will be encryption. + * @param {Stream} connection - streaming iterable duplex that will be encrypted. * @param {PeerId} remotePeer - optional PeerId of the initiating peer, if known. This may only exist during transport upgrades. - * @returns {Promise} + * @returns {Promise>} */ - public async secureInbound (localPeer: PeerId, connection: Duplex, AsyncIterable, Promise>, remotePeer?: PeerId): Promise> { + public async secureInbound > = MultiaddrConnection> (localPeer: PeerId, connection: Stream, remotePeer?: PeerId): Promise> { const wrappedConnection = lpStream( connection, { @@ -120,8 +125,11 @@ export class Noise implements INoiseConnection { }) const conn = await this.createSecureConnection(wrappedConnection, handshake) + connection.source = conn.source + connection.sink = conn.sink + return { - conn, + conn: connection, remotePeer: handshake.remotePeer, remoteExtensions: handshake.remoteExtensions } @@ -146,6 +154,7 @@ export class Noise implements INoiseConnection { ): Promise { const { isInitiator, remotePeer, connection } = params const handshake = new XXHandshake( + this.components, isInitiator, payload, this.prologue, @@ -172,11 +181,11 @@ export class Noise implements INoiseConnection { } private async createSecureConnection ( - connection: LengthPrefixedStream, AsyncIterable, Promise>>, + connection: LengthPrefixedStream>>, handshake: IHandshake - ): Promise, Source, Promise>> { + ): Promise>> { // Create encryption box/unbox wrapper - const [secure, user] = duplexPair() + const [secure, user] = duplexPair() const network = connection.unwrap() await pipe( diff --git a/src/utils.ts b/src/utils.ts index 993c962..d6fc849 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,11 @@ import { unmarshalPublicKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' import { peerIdFromKeys } from '@libp2p/peer-id' +import { type Uint8ArrayList, isUint8ArrayList } from 'uint8arraylist' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { type NoiseExtensions, NoiseHandshakePayload } from './proto/payload.js' import type { bytes } from './@types/basic.js' -import type { PeerId } from '@libp2p/interface/peer-id' +import type { PeerId } from '@libp2p/interface' export async function getPayload ( localPeer: PeerId, @@ -36,7 +37,7 @@ export function createHandshakePayload ( }).subarray() } -export async function signPayload (peerId: PeerId, payload: bytes): Promise { +export async function signPayload (peerId: PeerId, payload: Uint8Array | Uint8ArrayList): Promise { if (peerId.privateKey == null) { throw new Error('PrivateKey was missing from PeerId') } @@ -50,13 +51,20 @@ export async function getPeerIdFromPayload (payload: NoiseHandshakePayload): Pro return peerIdFromKeys(payload.identityKey) } -export function decodePayload (payload: bytes | Uint8Array): NoiseHandshakePayload { +export function decodePayload (payload: Uint8Array | Uint8ArrayList): NoiseHandshakePayload { return NoiseHandshakePayload.decode(payload) } -export function getHandshakePayload (publicKey: bytes): bytes { +export function getHandshakePayload (publicKey: Uint8Array | Uint8ArrayList): Uint8Array | Uint8ArrayList { const prefix = uint8ArrayFromString('noise-libp2p-static-key:') - return uint8ArrayConcat([prefix, publicKey], prefix.length + publicKey.length) + + if (publicKey instanceof Uint8Array) { + return uint8ArrayConcat([prefix, publicKey], prefix.length + publicKey.length) + } + + publicKey.prepend(prefix) + + return publicKey } /** @@ -68,7 +76,7 @@ export function getHandshakePayload (publicKey: bytes): bytes { * @returns {Promise} - peer ID of payload owner */ export async function verifySignedPayload ( - noiseStaticKey: bytes, + noiseStaticKey: Uint8Array | Uint8ArrayList, payload: NoiseHandshakePayload, remotePeer: PeerId ): Promise { @@ -98,12 +106,12 @@ export async function verifySignedPayload ( return payloadPeerId } -export function isValidPublicKey (pk: bytes): boolean { - if (!(pk instanceof Uint8Array)) { +export function isValidPublicKey (pk: Uint8Array | Uint8ArrayList): boolean { + if (!(pk instanceof Uint8Array) && !(isUint8ArrayList(pk))) { return false } - if (pk.length !== 32) { + if (pk.byteLength !== 32) { return false } diff --git a/test/compliance.spec.ts b/test/compliance.spec.ts index e97dc0a..bbda027 100644 --- a/test/compliance.spec.ts +++ b/test/compliance.spec.ts @@ -1,10 +1,11 @@ import tests from '@libp2p/interface-compliance-tests/connection-encryption' +import { defaultLogger } from '@libp2p/logger' import { Noise } from '../src/noise.js' describe('spec compliance tests', function () { tests({ async setup () { - return new Noise() + return new Noise({ logger: defaultLogger() }) }, async teardown () {} }) diff --git a/test/handshakes/xx.spec.ts b/test/handshakes/xx.spec.ts index 69e2733..40e1deb 100644 --- a/test/handshakes/xx.spec.ts +++ b/test/handshakes/xx.spec.ts @@ -1,4 +1,5 @@ import { Buffer } from 'buffer' +import { defaultLogger } from '@libp2p/logger' import { expect, assert } from 'aegir/chai' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' @@ -14,7 +15,7 @@ describe('XX Handshake', () => { it('Test creating new XX session', async () => { try { - const xx = new XX(pureJsCrypto) + const xx = new XX({ logger: defaultLogger() }, pureJsCrypto) const kpInitiator: KeyPair = pureJsCrypto.generateX25519KeyPair() @@ -107,7 +108,7 @@ describe('XX Handshake', () => { it('Test handshake', async () => { try { - const xx = new XX(pureJsCrypto) + const xx = new XX({ logger: defaultLogger() }, pureJsCrypto) await doHandshake(xx) } catch (e) { const err = e as Error @@ -117,7 +118,7 @@ describe('XX Handshake', () => { it('Test symmetric encrypt and decrypt', async () => { try { - const xx = new XX(pureJsCrypto) + const xx = new XX({ logger: defaultLogger() }, pureJsCrypto) const { nsInit, nsResp } = await doHandshake(xx) const ad = Buffer.from('authenticated') const message = Buffer.from('HelloCrypto') @@ -127,10 +128,10 @@ describe('XX Handshake', () => { } const ciphertext = xx.encryptWithAd(nsInit.cs1, ad, message) - assert(!uint8ArrayEquals(Buffer.from('HelloCrypto'), ciphertext), 'Encrypted message should not be same as plaintext.') + assert(!uint8ArrayEquals(Buffer.from('HelloCrypto'), ciphertext.subarray()), 'Encrypted message should not be same as plaintext.') const { plaintext: decrypted, valid } = xx.decryptWithAd(nsResp.cs1, ad, ciphertext) - assert(uint8ArrayEquals(Buffer.from('HelloCrypto'), decrypted), 'Decrypted text not equal to original message.') + assert(uint8ArrayEquals(Buffer.from('HelloCrypto'), decrypted.subarray()), 'Decrypted text not equal to original message.') assert(valid) } catch (e) { const err = e as Error @@ -139,7 +140,7 @@ describe('XX Handshake', () => { }) it('Test multiple messages encryption and decryption', async () => { - const xx = new XX(pureJsCrypto) + const xx = new XX({ logger: defaultLogger() }, pureJsCrypto) const { nsInit, nsResp } = await doHandshake(xx) const ad = Buffer.from('authenticated') const message = Buffer.from('ethereum1') @@ -150,11 +151,11 @@ describe('XX Handshake', () => { const encrypted = xx.encryptWithAd(nsInit.cs1, ad, message) const { plaintext: decrypted } = xx.decryptWithAd(nsResp.cs1, ad, encrypted) - assert.equal('ethereum1', uint8ArrayToString(decrypted, 'utf8'), 'Decrypted text not equal to original message.') + assert.equal('ethereum1', uint8ArrayToString(decrypted.subarray(), 'utf8'), 'Decrypted text not equal to original message.') const message2 = Buffer.from('ethereum2') const encrypted2 = xx.encryptWithAd(nsInit.cs1, ad, message2) const { plaintext: decrypted2 } = xx.decryptWithAd(nsResp.cs1, ad, encrypted2) - assert.equal('ethereum2', uint8ArrayToString(decrypted2, 'utf-8'), 'Decrypted text not equal to original message.') + assert.equal('ethereum2', uint8ArrayToString(decrypted2.subarray(), 'utf-8'), 'Decrypted text not equal to original message.') }) }) diff --git a/test/index.spec.ts b/test/index.spec.ts index 5ce901d..89d17e5 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,3 +1,4 @@ +import { defaultLogger } from '@libp2p/logger' import { expect } from 'aegir/chai' import { lpStream } from 'it-length-prefixed-stream' import { duplexPair } from 'it-pair/duplex' @@ -7,6 +8,7 @@ import { noise } from '../src/index.js' import { Noise } from '../src/noise.js' import { createPeerIdsFromFixtures } from './fixtures/peer.js' import type { Metrics } from '@libp2p/interface/metrics' +import type { Uint8ArrayList } from 'uint8arraylist' function createCounterSpy (): ReturnType { return sinon.spy({ @@ -17,7 +19,7 @@ function createCounterSpy (): ReturnType { describe('Index', () => { it('should expose class with tag and required functions', () => { - const noiseInstance = noise()() + const noiseInstance = noise()({ logger: defaultLogger() }) expect(noiseInstance.protocol).to.equal('/noise') expect(typeof (noiseInstance.secureInbound)).to.equal('function') expect(typeof (noiseInstance.secureOutbound)).to.equal('function') @@ -33,10 +35,10 @@ describe('Index', () => { return counter } } - const noiseInit = new Noise({ metrics: metrics as any as Metrics }) - const noiseResp = new Noise({}) + const noiseInit = new Noise({ logger: defaultLogger(), metrics: metrics as any as Metrics }) + const noiseResp = new Noise({ logger: defaultLogger() }) - const [inboundConnection, outboundConnection] = duplexPair() + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, inbound] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), noiseResp.secureInbound(remotePeer, inboundConnection, localPeer) diff --git a/test/interop.ts b/test/interop.ts index e664115..2d89cc9 100644 --- a/test/interop.ts +++ b/test/interop.ts @@ -13,7 +13,7 @@ import { path as p2pd } from 'go-libp2p' import { createLibp2p, type Libp2pOptions } from 'libp2p' import pDefer from 'p-defer' import { noise } from '../src/index.js' -import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerId } from '@libp2p/interface/peer-id' import type { SpawnOptions, Daemon, DaemonFactory } from '@libp2p/interop' async function createGoPeer (options: SpawnOptions): Promise { diff --git a/test/noise.spec.ts b/test/noise.spec.ts index 8b2c693..25f0046 100644 --- a/test/noise.spec.ts +++ b/test/noise.spec.ts @@ -1,4 +1,5 @@ import { Buffer } from 'buffer' +import { defaultLogger } from '@libp2p/logger' import { assert, expect } from 'aegir/chai' import { randomBytes } from 'iso-random-stream' import { byteStream } from 'it-byte-stream' @@ -18,6 +19,7 @@ import { createHandshakePayload, getHandshakePayload, getPayload, signPayload } import { createPeerIdsFromFixtures } from './fixtures/peer.js' import { getKeyPairFromPeerId } from './utils.js' import type { PeerId } from '@libp2p/interface/peer-id' +import type { Uint8ArrayList } from 'uint8arraylist' describe('Noise', () => { let remotePeer: PeerId, localPeer: PeerId @@ -33,10 +35,10 @@ describe('Noise', () => { it('should communicate through encrypted streams without noise pipes', async () => { try { - const noiseInit = new Noise({ staticNoiseKey: undefined, extensions: undefined }) - const noiseResp = new Noise({ staticNoiseKey: undefined, extensions: undefined }) + const noiseInit = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined, extensions: undefined }) + const noiseResp = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined, extensions: undefined }) - const [inboundConnection, outboundConnection] = duplexPair() + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, inbound] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), noiseResp.secureInbound(remotePeer, inboundConnection, localPeer) @@ -54,8 +56,8 @@ describe('Noise', () => { }) it('should test that secureOutbound is spec compliant', async () => { - const noiseInit = new Noise({ staticNoiseKey: undefined }) - const [inboundConnection, outboundConnection] = duplexPair() + const noiseInit = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined }) + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, { wrapped, handshake }] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), @@ -70,10 +72,10 @@ describe('Noise', () => { ) const prologue = Buffer.alloc(0) const staticKeys = pureJsCrypto.generateX25519KeyPair() - const xx = new XX(pureJsCrypto) + const xx = new XX({ logger: defaultLogger() }, pureJsCrypto) const payload = await getPayload(remotePeer, staticKeys.publicKey) - const handshake = new XXHandshake(false, payload, prologue, pureJsCrypto, staticKeys, wrapped, localPeer, xx) + const handshake = new XXHandshake({ logger: defaultLogger() }, false, payload, prologue, pureJsCrypto, staticKeys, wrapped, localPeer, xx) let receivedMessageBuffer = decode0((await wrapped.read()).slice()) // The first handshake message contains the initiator's ephemeral public key @@ -82,7 +84,7 @@ describe('Noise', () => { // Stage 1 const { publicKey: libp2pPubKey } = getKeyPairFromPeerId(remotePeer) - const signedPayload = await signPayload(remotePeer, getHandshakePayload(staticKeys.publicKey)) + const signedPayload = await signPayload(remotePeer, getHandshakePayload(staticKeys.publicKey).subarray()) const handshakePayload = createHandshakePayload(libp2pPubKey, signedPayload) const messageBuffer = xx.sendMessage(handshake.session, handshakePayload) @@ -102,17 +104,17 @@ describe('Noise', () => { const data = (await wrapped.read()).slice() const { plaintext: decrypted, valid } = handshake.decrypt(data, handshake.session) // Decrypted data should match - expect(uint8ArrayEquals(decrypted, uint8ArrayFromString('test'))).to.be.true() + expect(uint8ArrayEquals(decrypted.subarray(), uint8ArrayFromString('test'))).to.be.true() expect(valid).to.be.true() }) it('should test large payloads', async function () { this.timeout(10000) try { - const noiseInit = new Noise({ staticNoiseKey: undefined }) - const noiseResp = new Noise({ staticNoiseKey: undefined }) + const noiseInit = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined }) + const noiseResp = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined }) - const [inboundConnection, outboundConnection] = duplexPair() + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, inbound] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), noiseResp.secureInbound(remotePeer, inboundConnection, localPeer) @@ -134,11 +136,11 @@ describe('Noise', () => { it('should working without remote peer provided in incoming connection', async () => { try { const staticKeysInitiator = pureJsCrypto.generateX25519KeyPair() - const noiseInit = new Noise({ staticNoiseKey: staticKeysInitiator.privateKey }) + const noiseInit = new Noise({ logger: defaultLogger() }, { staticNoiseKey: staticKeysInitiator.privateKey }) const staticKeysResponder = pureJsCrypto.generateX25519KeyPair() - const noiseResp = new Noise({ staticNoiseKey: staticKeysResponder.privateKey }) + const noiseResp = new Noise({ logger: defaultLogger() }, { staticNoiseKey: staticKeysResponder.privateKey }) - const [inboundConnection, outboundConnection] = duplexPair() + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, inbound] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), noiseResp.secureInbound(remotePeer, inboundConnection) @@ -167,12 +169,12 @@ describe('Noise', () => { try { const certhashInit = Buffer.from('certhash data from init') const staticKeysInitiator = pureJsCrypto.generateX25519KeyPair() - const noiseInit = new Noise({ staticNoiseKey: staticKeysInitiator.privateKey, extensions: { webtransportCerthashes: [certhashInit] } }) + const noiseInit = new Noise({ logger: defaultLogger() }, { staticNoiseKey: staticKeysInitiator.privateKey, extensions: { webtransportCerthashes: [certhashInit] } }) const staticKeysResponder = pureJsCrypto.generateX25519KeyPair() const certhashResp = Buffer.from('certhash data from respon') - const noiseResp = new Noise({ staticNoiseKey: staticKeysResponder.privateKey, extensions: { webtransportCerthashes: [certhashResp] } }) + const noiseResp = new Noise({ logger: defaultLogger() }, { staticNoiseKey: staticKeysResponder.privateKey, extensions: { webtransportCerthashes: [certhashResp] } }) - const [inboundConnection, outboundConnection] = duplexPair() + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, inbound] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), noiseResp.secureInbound(remotePeer, inboundConnection) @@ -188,10 +190,10 @@ describe('Noise', () => { it('should accept a prologue', async () => { try { - const noiseInit = new Noise({ staticNoiseKey: undefined, crypto: pureJsCrypto, prologueBytes: Buffer.from('Some prologue') }) - const noiseResp = new Noise({ staticNoiseKey: undefined, crypto: pureJsCrypto, prologueBytes: Buffer.from('Some prologue') }) + const noiseInit = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined, crypto: pureJsCrypto, prologueBytes: Buffer.from('Some prologue') }) + const noiseResp = new Noise({ logger: defaultLogger() }, { staticNoiseKey: undefined, crypto: pureJsCrypto, prologueBytes: Buffer.from('Some prologue') }) - const [inboundConnection, outboundConnection] = duplexPair() + const [inboundConnection, outboundConnection] = duplexPair() const [outbound, inbound] = await Promise.all([ noiseInit.secureOutbound(localPeer, outboundConnection, remotePeer), noiseResp.secureInbound(remotePeer, inboundConnection, localPeer) diff --git a/test/xx-handshake.spec.ts b/test/xx-handshake.spec.ts index ef2c000..56d0958 100644 --- a/test/xx-handshake.spec.ts +++ b/test/xx-handshake.spec.ts @@ -1,4 +1,5 @@ import { Buffer } from 'buffer' +import { defaultLogger } from '@libp2p/logger' import { assert, expect } from 'aegir/chai' import { lpStream } from 'it-length-prefixed-stream' import { duplexPair } from 'it-pair/duplex' @@ -27,10 +28,10 @@ describe('XX Handshake', () => { const staticKeysResponder = defaultCrypto.generateX25519KeyPair() const initPayload = await getPayload(peerA, staticKeysInitiator.publicKey) - const handshakeInitiator = new XXHandshake(true, initPayload, prologue, defaultCrypto, staticKeysInitiator, connectionFrom, peerB) + const handshakeInitiator = new XXHandshake({ logger: defaultLogger() }, true, initPayload, prologue, defaultCrypto, staticKeysInitiator, connectionFrom, peerB) const respPayload = await getPayload(peerB, staticKeysResponder.publicKey) - const handshakeResponder = new XXHandshake(false, respPayload, prologue, defaultCrypto, staticKeysResponder, connectionTo, peerA) + const handshakeResponder = new XXHandshake({ logger: defaultLogger() }, false, respPayload, prologue, defaultCrypto, staticKeysResponder, connectionTo, peerA) await Promise.all([ handshakeInitiator.propose(), @@ -61,7 +62,7 @@ describe('XX Handshake', () => { // Test encryption and decryption const encrypted = handshakeInitiator.encrypt(Buffer.from('encryptthis'), handshakeInitiator.session) const { plaintext: decrypted, valid } = handshakeResponder.decrypt(encrypted, handshakeResponder.session) - assert(uint8ArrayEquals(decrypted, Buffer.from('encryptthis'))) + assert(uint8ArrayEquals(decrypted.subarray(), Buffer.from('encryptthis'))) assert(valid) } catch (e) { const err = e as Error @@ -80,10 +81,10 @@ describe('XX Handshake', () => { const staticKeysResponder = defaultCrypto.generateX25519KeyPair() const initPayload = await getPayload(peerA, staticKeysInitiator.publicKey) - const handshakeInitiator = new XXHandshake(true, initPayload, prologue, defaultCrypto, staticKeysInitiator, connectionFrom, fakePeer) + const handshakeInitiator = new XXHandshake({ logger: defaultLogger() }, true, initPayload, prologue, defaultCrypto, staticKeysInitiator, connectionFrom, fakePeer) const respPayload = await getPayload(peerB, staticKeysResponder.publicKey) - const handshakeResponder = new XXHandshake(false, respPayload, prologue, defaultCrypto, staticKeysResponder, connectionTo, peerA) + const handshakeResponder = new XXHandshake({ logger: defaultLogger() }, false, respPayload, prologue, defaultCrypto, staticKeysResponder, connectionTo, peerA) await Promise.all([ handshakeInitiator.propose(), @@ -113,10 +114,10 @@ describe('XX Handshake', () => { const staticKeysResponder = defaultCrypto.generateX25519KeyPair() const initPayload = await getPayload(peerA, staticKeysInitiator.publicKey) - const handshakeInitiator = new XXHandshake(true, initPayload, prologue, defaultCrypto, staticKeysInitiator, connectionFrom, peerB) + const handshakeInitiator = new XXHandshake({ logger: defaultLogger() }, true, initPayload, prologue, defaultCrypto, staticKeysInitiator, connectionFrom, peerB) const respPayload = await getPayload(peerB, staticKeysResponder.publicKey) - const handshakeResponder = new XXHandshake(false, respPayload, prologue, defaultCrypto, staticKeysResponder, connectionTo, fakePeer) + const handshakeResponder = new XXHandshake({ logger: defaultLogger() }, false, respPayload, prologue, defaultCrypto, staticKeysResponder, connectionTo, fakePeer) await Promise.all([ handshakeInitiator.propose(),