Merge pull request #18 from NodeFactoryIo/feature/noise-pipes

Noise pipes
This commit is contained in:
Belma Gutlic 2020-01-17 23:54:29 +01:00 committed by GitHub
commit 25e2e96c5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 97 additions and 23 deletions

View File

@ -24,6 +24,7 @@ export class IKHandshake implements IHandshake {
staticKeypair: KeyPair, staticKeypair: KeyPair,
connection: WrappedConnection, connection: WrappedConnection,
remotePeer: PeerId, remotePeer: PeerId,
remoteStaticKey: bytes,
handshake?: IK, handshake?: IK,
) { ) {
this.isInitiator = isInitiator; this.isInitiator = isInitiator;
@ -34,11 +35,7 @@ export class IKHandshake implements IHandshake {
this.remotePeer = remotePeer; this.remotePeer = remotePeer;
this.ik = handshake || new IK(); this.ik = handshake || new IK();
this.session = this.ik.initSession(this.isInitiator, this.prologue, this.staticKeypair, remoteStaticKey);
// Dummy data
// TODO: Load remote static keys if found
const remoteStaticKeys = this.staticKeypair;
this.session = this.ik.initSession(this.isInitiator, this.prologue, this.staticKeypair, remoteStaticKeys.publicKey);
} }
public decrypt(ciphertext: Buffer, session: NoiseSession): Buffer { public decrypt(ciphertext: Buffer, session: NoiseSession): Buffer {

View File

@ -114,6 +114,10 @@ export class XXHandshake implements IHandshake {
return this.xx.decryptWithAd(cs, Buffer.alloc(0), ciphertext); return this.xx.decryptWithAd(cs, Buffer.alloc(0), ciphertext);
} }
public getRemoteStaticKey(): bytes {
return this.session.hs.rs;
}
private getCS(session: NoiseSession, encryption = true) { private getCS(session: NoiseSession, encryption = true) {
if (!session.cs1 || !session.cs2) { if (!session.cs1 || !session.cs2) {
throw new Error("Handshake not completed properly, cipher state does not exist."); throw new Error("Handshake not completed properly, cipher state does not exist.");

View File

@ -145,7 +145,6 @@ export class XX extends AbstractHandshake {
session.h = h; session.h = h;
session.cs1 = cs1; session.cs1 = cs1;
session.cs2 = cs2; session.cs2 = cs2;
delete session.hs;
} else if (session.mc.gtn(2)) { } else if (session.mc.gtn(2)) {
if (session.i) { if (session.i) {
if (!session.cs1) { if (!session.cs1) {

27
src/keycache.ts Normal file
View File

@ -0,0 +1,27 @@
import {PeerId} from "./@types/libp2p";
import {bytes, bytes32} from "./@types/basic";
/**
* Storage for static keys of previously connected peers.
*/
class Keycache {
private storage = new Map<bytes, bytes32>();
public async store(peerId: PeerId, key: bytes32): Promise<void> {
this.storage.set(peerId.id, key);
}
public async load(peerId: PeerId): Promise<bytes32|null> {
return this.storage.get(peerId.id) || null;
}
public resetStorage(): void {
this.storage.clear();
}
}
const KeyCache = new Keycache();
export {
KeyCache,
}

View File

@ -16,6 +16,8 @@ import { bytes } from "./@types/basic";
import { INoiseConnection, PeerId, KeyPair, SecureOutbound } from "./@types/libp2p"; import { INoiseConnection, PeerId, KeyPair, SecureOutbound } from "./@types/libp2p";
import { Duplex } from "./@types/it-pair"; import { Duplex } from "./@types/it-pair";
import {IHandshake} from "./@types/handshake-interface"; import {IHandshake} from "./@types/handshake-interface";
import {KeyCache} from "./keycache";
import {logger} from "./logger";
export type WrappedConnection = ReturnType<typeof Wrap>; export type WrappedConnection = ReturnType<typeof Wrap>;
@ -32,9 +34,11 @@ export class Noise implements INoiseConnection {
private readonly prologue = Buffer.from(this.protocol); private readonly prologue = Buffer.from(this.protocol);
private readonly staticKeys: KeyPair; private readonly staticKeys: KeyPair;
private readonly earlyData?: bytes; private readonly earlyData?: bytes;
private useNoisePipes: boolean;
constructor(staticNoiseKey?: bytes, earlyData?: bytes) { constructor(staticNoiseKey?: bytes, earlyData?: bytes, useNoisePipes = true) {
this.earlyData = earlyData || Buffer.alloc(0); this.earlyData = earlyData || Buffer.alloc(0);
this.useNoisePipes = useNoisePipes;
if (staticNoiseKey) { if (staticNoiseKey) {
const publicKey = x25519.publicKeyCreate(staticNoiseKey); // TODO: verify this const publicKey = x25519.publicKeyCreate(staticNoiseKey); // TODO: verify this
@ -95,27 +99,35 @@ export class Noise implements INoiseConnection {
/** /**
* If Noise pipes supported, tries IK handshake first with XX as fallback if it fails. * If Noise pipes supported, tries IK handshake first with XX as fallback if it fails.
* If remote peer static key is unknown, use XX. * If noise pipes disabled or remote peer static key is unknown, use XX.
* @param connection * @param connection
* @param isInitiator * @param isInitiator
* @param libp2pPublicKey * @param libp2pPublicKey
* @param remotePeer * @param remotePeer
*/ */
private async performHandshake(params: HandshakeParams): Promise<IHandshake> { private async performHandshake(params: HandshakeParams): Promise<IHandshake> {
// TODO: Implement noise pipes
const payload = await getPayload(params.localPeer, this.staticKeys.publicKey, this.earlyData); const payload = await getPayload(params.localPeer, this.staticKeys.publicKey, this.earlyData);
if (false) { let foundRemoteStaticKey: bytes|null = null;
let IKhandshake; if (this.useNoisePipes && params.isInitiator) {
logger("Initiator using noise pipes. Going to load cached static key...");
foundRemoteStaticKey = await KeyCache.load(params.remotePeer);
logger(`Static key has been found: ${!!foundRemoteStaticKey}`)
}
if (foundRemoteStaticKey) {
// Try IK first
const { remotePeer, connection, isInitiator } = params;
const IKhandshake = new IKHandshake(isInitiator, payload, this.prologue, this.staticKeys, connection, remotePeer, foundRemoteStaticKey);
try { try {
IKhandshake = await this.performIKHandshake(params, payload); return await this.performIKHandshake(IKhandshake, payload);
return IKhandshake;
} catch (e) { } catch (e) {
// XX fallback // IK failed, go to XX fallback
const ephemeralKeys = IKhandshake.getRemoteEphemeralKeys(); const ephemeralKeys = IKhandshake.getRemoteEphemeralKeys();
return await this.performXXFallbackHandshake(params, payload, ephemeralKeys, e.initialMsg); return await this.performXXFallbackHandshake(params, payload, ephemeralKeys, e.initialMsg);
} }
} else { } else {
// Noise pipes not supported, use XX
return await this.performXXHandshake(params, payload); return await this.performXXHandshake(params, payload);
} }
} }
@ -152,6 +164,10 @@ export class Noise implements INoiseConnection {
await handshake.propose(); await handshake.propose();
await handshake.exchange(); await handshake.exchange();
await handshake.finish(); await handshake.finish();
if (this.useNoisePipes) {
await KeyCache.store(remotePeer, handshake.getRemoteStaticKey());
}
} catch (e) { } catch (e) {
throw new Error(`Error occurred during XX handshake: ${e.message}`); throw new Error(`Error occurred during XX handshake: ${e.message}`);
} }
@ -160,12 +176,9 @@ export class Noise implements INoiseConnection {
} }
private async performIKHandshake( private async performIKHandshake(
params: HandshakeParams, handshake: IKHandshake,
payload: bytes, payload: bytes,
): Promise<IKHandshake> { ): Promise<IKHandshake> {
const { isInitiator, remotePeer, connection } = params;
const handshake = new IKHandshake(isInitiator, payload, this.prologue, this.staticKeys, connection, remotePeer);
// TODO // TODO
return handshake; return handshake;

34
test/keycache.test.ts Normal file
View File

@ -0,0 +1,34 @@
import { expect, assert } from "chai";
import { KeyCache } from "../src/keycache";
import {createPeerIds, createPeerIdsFromFixtures} from "./fixtures/peer";
describe("KeyCache", () => {
let peerA, peerB;
before(async () => {
[peerA, peerB] = await createPeerIdsFromFixtures(2);
});
it("should store and load same key successfully", async() => {
try {
const key = Buffer.from("this is id 007");
await KeyCache.store(peerA, key);
const result = await KeyCache.load(peerA);
assert(result.equals(key), "Stored and loaded key are not the same");
} catch (e) {
console.error(e);
assert(false, `Test failed - ${e.message}`)
}
});
it("should return undefined if key not found", async() => {
try {
const [newPeer] = await createPeerIds(1);
const result = await KeyCache.load(newPeer);
assert(!result);
} catch (e) {
console.error(e);
assert(false, `Test failed - ${e.message}`)
}
});
});

View File

@ -26,8 +26,8 @@ describe("Noise", () => {
it("should communicate through encrypted streams", async() => { it("should communicate through encrypted streams", async() => {
try { try {
const noiseInit = new Noise(); const noiseInit = new Noise(undefined, undefined, false);
const noiseResp = new Noise(); const noiseResp = new Noise(undefined, undefined, false);
const [inboundConnection, outboundConnection] = DuplexPair(); const [inboundConnection, outboundConnection] = DuplexPair();
const [outbound, inbound] = await Promise.all([ const [outbound, inbound] = await Promise.all([
@ -46,7 +46,7 @@ describe("Noise", () => {
}); });
it("should test that secureOutbound is spec compliant", async() => { it("should test that secureOutbound is spec compliant", async() => {
const noiseInit = new Noise(); const noiseInit = new Noise(undefined, undefined, false);
const [inboundConnection, outboundConnection] = DuplexPair(); const [inboundConnection, outboundConnection] = DuplexPair();
const [outbound, { wrapped, handshake }] = await Promise.all([ const [outbound, { wrapped, handshake }] = await Promise.all([
@ -99,8 +99,8 @@ describe("Noise", () => {
it("should test large payloads", async() => { it("should test large payloads", async() => {
try { try {
const noiseInit = new Noise(); const noiseInit = new Noise(undefined, undefined, false);
const noiseResp = new Noise(); const noiseResp = new Noise(undefined, undefined, false);
const [inboundConnection, outboundConnection] = DuplexPair(); const [inboundConnection, outboundConnection] = DuplexPair();
const [outbound, inbound] = await Promise.all([ const [outbound, inbound] = await Promise.all([