@chainsafe/libp2p-noise
Version:
Noise libp2p handshake for js-libp2p
174 lines • 7.33 kB
JavaScript
import { publicKeyFromProtobuf } from '@libp2p/crypto/keys';
import { InvalidCryptoExchangeError, serviceCapabilities } from '@libp2p/interface';
import { peerIdFromPublicKey } from '@libp2p/peer-id';
import { lpStream } from '@libp2p/utils';
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc';
import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.js';
import { defaultCrypto } from './crypto/index.js';
import { wrapCrypto } from './crypto.js';
import { uint16BEDecode, uint16BEEncode } from './encoder.js';
import { registerMetrics } from './metrics.js';
import { performHandshakeInitiator, performHandshakeResponder } from './performHandshake.js';
import { toMessageStream } from "./utils.js";
export class Noise {
protocol = '/noise';
crypto;
prologue;
staticKey;
extensions;
metrics;
components;
log;
constructor(components, init = {}) {
const { staticNoiseKey, extensions, crypto, prologueBytes } = init;
const { metrics } = components;
this.components = components;
this.log = components.logger.forComponent('libp2p:noise');
const _crypto = crypto ?? defaultCrypto;
this.crypto = wrapCrypto(_crypto);
this.extensions = {
webtransportCerthashes: [],
...extensions
};
this.metrics = metrics ? registerMetrics(metrics) : undefined;
if (staticNoiseKey) {
// accepts x25519 private key of length 32
this.staticKey = _crypto.generateX25519KeyPairFromSeed(staticNoiseKey);
}
else {
this.staticKey = _crypto.generateX25519KeyPair();
}
this.prologue = prologueBytes ?? uint8ArrayAlloc(0);
}
[Symbol.toStringTag] = '@chainsafe/libp2p-noise';
[serviceCapabilities] = [
'@libp2p/connection-encryption',
'@chainsafe/libp2p-noise'
];
/**
* Encrypt outgoing data to the remote party (handshake as initiator)
*
* @param connection - streaming iterable duplex that will be encrypted
* @param options
* @param options.remotePeer - PeerId of the remote peer. Used to validate the integrity of the remote peer
* @param options.signal - Used to abort the operation
*/
async secureOutbound(connection, options) {
const log = connection.log?.newScope('noise') ?? this.log;
const wrappedConnection = lpStream(connection, {
lengthEncoder: uint16BEEncode,
lengthDecoder: uint16BEDecode,
maxDataLength: NOISE_MSG_MAX_LENGTH_BYTES
});
const handshake = await this.performHandshakeInitiator(wrappedConnection, this.components.privateKey, log, options?.remotePeer?.publicKey, options);
const publicKey = publicKeyFromProtobuf(handshake.payload.identityKey);
return {
connection: toMessageStream(wrappedConnection.unwrap(), handshake, this.metrics),
remoteExtensions: handshake.payload.extensions,
remotePeer: peerIdFromPublicKey(publicKey),
streamMuxer: options?.skipStreamMuxerNegotiation === true ? undefined : this.getStreamMuxer(handshake.payload.extensions?.streamMuxers)
};
}
getStreamMuxer(protocols) {
if (protocols == null || protocols.length === 0) {
return;
}
const streamMuxers = this.components.upgrader.getStreamMuxers();
if (streamMuxers != null) {
for (const protocol of protocols) {
const streamMuxer = streamMuxers.get(protocol);
if (streamMuxer != null) {
return streamMuxer;
}
}
}
if (protocols.length) {
throw new InvalidCryptoExchangeError('Early muxer negotiation was requested but the initiator and responder had no common muxers');
}
}
/**
* Decrypt incoming data (handshake as responder).
*
* @param connection - streaming iterable duplex that will be encrypted
* @param options
* @param options.remotePeer - PeerId of the remote peer. Used to validate the integrity of the remote peer
* @param options.signal - Used to abort the operation
*/
async secureInbound(connection, options) {
const log = connection.log?.newScope('noise') ?? this.log;
const wrappedConnection = lpStream(connection, {
lengthEncoder: uint16BEEncode,
lengthDecoder: uint16BEDecode,
maxDataLength: NOISE_MSG_MAX_LENGTH_BYTES
});
const handshake = await this.performHandshakeResponder(wrappedConnection, this.components.privateKey, log, options?.remotePeer?.publicKey, options);
const publicKey = publicKeyFromProtobuf(handshake.payload.identityKey);
return {
connection: toMessageStream(wrappedConnection.unwrap(), handshake, this.metrics),
remoteExtensions: handshake.payload.extensions,
remotePeer: peerIdFromPublicKey(publicKey),
streamMuxer: options?.skipStreamMuxerNegotiation === true ? undefined : this.getStreamMuxer(handshake.payload.extensions?.streamMuxers)
};
}
/**
* Perform XX handshake as initiator.
*/
async performHandshakeInitiator(connection,
// TODO: pass private key in noise constructor via Components
privateKey, log, remoteIdentityKey, options) {
let result;
const streamMuxers = options?.skipStreamMuxerNegotiation === true ? [] : [...this.components.upgrader.getStreamMuxers().keys()];
try {
result = await performHandshakeInitiator({
connection,
privateKey,
remoteIdentityKey,
log: log.newScope('xxhandshake'),
crypto: this.crypto,
prologue: this.prologue,
s: this.staticKey,
extensions: {
streamMuxers,
webtransportCerthashes: [],
...this.extensions
}
}, options);
this.metrics?.xxHandshakeSuccesses.increment();
}
catch (e) {
this.metrics?.xxHandshakeErrors.increment();
throw e;
}
return result;
}
/**
* Perform XX handshake as responder.
*/
async performHandshakeResponder(connection, privateKey, log, remoteIdentityKey, options) {
let result;
const streamMuxers = options?.skipStreamMuxerNegotiation === true ? [] : [...this.components.upgrader.getStreamMuxers().keys()];
try {
result = await performHandshakeResponder({
connection,
privateKey,
remoteIdentityKey,
log: log.newScope('xxhandshake'),
crypto: this.crypto,
prologue: this.prologue,
s: this.staticKey,
extensions: {
streamMuxers,
webtransportCerthashes: [],
...this.extensions
}
}, options);
this.metrics?.xxHandshakeSuccesses.increment();
}
catch (e) {
this.metrics?.xxHandshakeErrors.increment();
throw e;
}
return result;
}
}
//# sourceMappingURL=noise.js.map