Merge pull request #222 from status-im/eth-dm-observer-infinite-loop

This commit is contained in:
Franck Royer 2021-07-09 14:48:39 +10:00 committed by GitHub
commit 7c47a6b215
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 174 additions and 167 deletions

View File

@ -10,7 +10,6 @@ import { Message } from './messaging/Messages';
import 'fontsource-roboto';
import { AppBar, IconButton, Toolbar, Typography } from '@material-ui/core';
import KeyPairHandling from './key_pair_handling/KeyPairHandling';
import InitWaku from './InitWaku';
import {
createMuiTheme,
ThemeProvider,
@ -20,6 +19,13 @@ import { teal, purple, green } from '@material-ui/core/colors';
import WifiIcon from '@material-ui/icons/Wifi';
import BroadcastPublicKey from './BroadcastPublicKey';
import Messaging from './messaging/Messaging';
import {
DirectMessageContentTopic,
handleDirectMessage,
handlePublicKeyMessage,
initWaku,
PublicKeyContentTopic,
} from './waku';
declare let window: any;
@ -85,6 +91,62 @@ function App() {
}
}, [address, signer]);
// Waku initialization
useEffect(() => {
if (waku) return;
initWaku()
.then((_waku) => {
console.log('waku: ready');
setWaku(_waku);
})
.catch((e) => {
console.error('Failed to initiate Waku', e);
});
}, [waku]);
useEffect(() => {
if (!waku) return;
if (!address) return;
const observerPublicKeyMessage = handlePublicKeyMessage.bind(
{},
address,
setPublicKeys
);
waku.relay.addObserver(observerPublicKeyMessage, [PublicKeyContentTopic]);
return function cleanUp() {
if (!waku) return;
waku.relay.deleteObserver(observerPublicKeyMessage, [
PublicKeyContentTopic,
]);
};
}, [waku, address]);
useEffect(() => {
if (!waku) return;
if (!ethDmKeyPair) return;
if (!address) return;
const observerDirectMessage = handleDirectMessage.bind(
{},
setMessages,
ethDmKeyPair.privateKey,
address
);
waku.relay.addObserver(observerDirectMessage, [DirectMessageContentTopic]);
return function cleanUp() {
if (!waku) return;
if (!observerDirectMessage) return;
waku.relay.deleteObserver(observerDirectMessage, [
DirectMessageContentTopic,
]);
};
}, [waku, address, ethDmKeyPair]);
let peers = 0;
if (waku) {
peers = waku.libp2p.connectionManager.connections.size;
@ -123,14 +185,6 @@ function App() {
<div className={classes.container}>
<main className={classes.main}>
<InitWaku
ethDmKeyPair={ethDmKeyPair}
setMessages={setMessages}
setPublicKeys={setPublicKeys}
setWaku={setWaku}
waku={waku}
address={address}
/>
<fieldset>
<legend>Eth-DM Key Pair</legend>
<KeyPairHandling

View File

@ -4,7 +4,7 @@ import { createPublicKeyMessage, KeyPair } from './crypto';
import { PublicKeyMessage } from './messaging/wire';
import { WakuMessage, Waku } from 'js-waku';
import { Signer } from '@ethersproject/abstract-signer';
import { PublicKeyContentTopic } from './InitWaku';
import { PublicKeyContentTopic } from './waku';
interface Props {
ethDmKeyPair: KeyPair | undefined;

View File

@ -1,156 +0,0 @@
import { Dispatch, SetStateAction, useEffect } from 'react';
import { Environment, getStatusFleetNodes, Waku, WakuMessage } from 'js-waku';
import { decode, DirectMessage, PublicKeyMessage } from './messaging/wire';
import { decryptMessage, KeyPair, validatePublicKeyMessage } from './crypto';
import { Message } from './messaging/Messages';
import { byteArrayToHex } from './utils';
export const PublicKeyContentTopic = '/eth-dm/1/public-key/proto';
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
interface Props {
waku: Waku | undefined;
setWaku: (waku: Waku) => void;
ethDmKeyPair: KeyPair | undefined;
setPublicKeys: Dispatch<SetStateAction<Map<string, string>>>;
setMessages: Dispatch<SetStateAction<Message[]>>;
address: string | undefined;
}
/**
* Does all the waku initialization
*/
export default function InitWaku({
waku,
setWaku,
ethDmKeyPair,
setPublicKeys,
setMessages,
address,
}: Props) {
useEffect(() => {
if (waku) return;
initWaku()
.then((wakuNode) => {
console.log('waku: ready');
setWaku(wakuNode);
})
.catch((e) => {
console.error('Failed to initiate Waku', e);
});
}, [waku, setWaku]);
const observerPublicKeyMessage = handlePublicKeyMessage.bind(
{},
ethDmKeyPair?.publicKey,
setPublicKeys
);
const observerDirectMessage =
ethDmKeyPair && address
? handleDirectMessage.bind(
{},
setMessages,
ethDmKeyPair.privateKey,
address
)
: undefined;
useEffect(() => {
if (!waku) return;
waku.relay.addObserver(observerPublicKeyMessage, [PublicKeyContentTopic]);
return function cleanUp() {
if (!waku) return;
waku.relay.deleteObserver(observerPublicKeyMessage, [
PublicKeyContentTopic,
]);
};
});
useEffect(() => {
if (!waku) return;
if (!observerDirectMessage) return;
waku.relay.addObserver(observerDirectMessage, [DirectMessageContentTopic]);
return function cleanUp() {
if (!waku) return;
if (!observerDirectMessage) return;
waku.relay.deleteObserver(observerDirectMessage, [
DirectMessageContentTopic,
]);
};
});
// Returns an empty fragment.
// Taking advantages of React's state management and useEffect()
// Not sure it is best practice but it works.
return <></>;
}
async function initWaku(): Promise<Waku> {
const waku = await Waku.create({});
const nodes = await getNodes();
await Promise.all(
nodes.map((addr) => {
return waku.dial(addr);
})
);
return waku;
}
function getNodes() {
return getStatusFleetNodes(Environment.Prod);
}
function handlePublicKeyMessage(
myPublicKey: string | undefined,
setter: Dispatch<SetStateAction<Map<string, string>>>,
msg: WakuMessage
) {
console.log('Public Key Message received:', msg);
if (!msg.payload) return;
const publicKeyMsg = PublicKeyMessage.decode(msg.payload);
if (!publicKeyMsg) return;
const ethDmPublicKey = byteArrayToHex(publicKeyMsg.ethDmPublicKey);
if (ethDmPublicKey === myPublicKey) return;
const res = validatePublicKeyMessage(publicKeyMsg);
console.log('Is Public Key Message valid?', res);
if (res) {
setter((prevPks: Map<string, string>) => {
prevPks.set(byteArrayToHex(publicKeyMsg.ethAddress), ethDmPublicKey);
return new Map(prevPks);
});
}
}
async function handleDirectMessage(
setter: Dispatch<SetStateAction<Message[]>>,
privateKey: string,
address: string,
wakuMsg: WakuMessage
) {
console.log('Direct Message received:', wakuMsg);
if (!wakuMsg.payload) return;
const directMessage: DirectMessage = decode(wakuMsg.payload);
// Do not return our own messages
if (directMessage.toAddress === address) return;
const text = await decryptMessage(privateKey, directMessage);
const timestamp = wakuMsg.timestamp ? wakuMsg.timestamp : new Date();
console.log('Message decrypted:', text);
setter((prevMsgs: Message[]) => {
const copy = prevMsgs.slice();
copy.push({
text: text,
timestamp: timestamp,
});
return copy;
});
}

View File

@ -10,7 +10,7 @@ import React, { ChangeEvent, useState, KeyboardEvent } from 'react';
import { Waku, WakuMessage } from 'js-waku';
import { DirectMessage, encode } from './wire';
import { encryptMessage } from '../crypto';
import { DirectMessageContentTopic } from '../InitWaku';
import { DirectMessageContentTopic } from '../waku';
const useStyles = makeStyles((theme) => ({
formControl: {

View File

@ -6,3 +6,24 @@ export function byteArrayToHex(bytes: Uint8Array): string {
export function hexToBuf(str: string): Buffer {
return Buffer.from(str.replace(/0x/, ''), 'hex');
}
export function equalByteArrays(
a: Uint8Array | Buffer | string,
b: Uint8Array | Buffer | string
): boolean {
let aBuf: Buffer;
let bBuf: Buffer;
if (typeof a === 'string') {
aBuf = hexToBuf(a);
} else {
aBuf = Buffer.from(a);
}
if (typeof b === 'string') {
bBuf = hexToBuf(b);
} else {
bBuf = Buffer.from(b);
}
return aBuf.compare(bBuf) === 0;
}

View File

@ -0,0 +1,88 @@
import { Dispatch, SetStateAction } from 'react';
import { getStatusFleetNodes, Waku, WakuMessage } from 'js-waku';
import { decode, DirectMessage, PublicKeyMessage } from './messaging/wire';
import { decryptMessage, validatePublicKeyMessage } from './crypto';
import { Message } from './messaging/Messages';
import { byteArrayToHex, equalByteArrays } from './utils';
export const PublicKeyContentTopic = '/eth-dm/1/public-key/proto';
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
export async function initWaku(): Promise<Waku> {
const waku = await Waku.create({});
// Dial all nodes it can find
getStatusFleetNodes().then((nodes) => {
nodes.forEach((addr) => {
waku.dial(addr);
});
});
// Wait to be connected to at least one peer
await new Promise((resolve, reject) => {
// If we are not connected to any peer within 10sec let's just reject
// As we are not implementing connection management in this example
setTimeout(reject, 10000);
waku.libp2p.connectionManager.on('peer:connect', () => {
resolve(null);
});
});
return waku;
}
export function handlePublicKeyMessage(
myAddress: string,
setter: Dispatch<SetStateAction<Map<string, string>>>,
msg: WakuMessage
) {
console.log('Public Key Message received:', msg);
if (!msg.payload) return;
const publicKeyMsg = PublicKeyMessage.decode(msg.payload);
if (!publicKeyMsg) return;
const ethDmPublicKey = byteArrayToHex(publicKeyMsg.ethDmPublicKey);
console.log(ethDmPublicKey, myAddress);
if (myAddress && equalByteArrays(publicKeyMsg.ethAddress, myAddress)) return;
const res = validatePublicKeyMessage(publicKeyMsg);
console.log('Is Public Key Message valid?', res);
if (res) {
setter((prevPks: Map<string, string>) => {
prevPks.set(byteArrayToHex(publicKeyMsg.ethAddress), ethDmPublicKey);
return new Map(prevPks);
});
}
}
export async function handleDirectMessage(
setter: Dispatch<SetStateAction<Message[]>>,
privateKey: string,
address: string,
wakuMsg: WakuMessage
) {
console.log('Direct Message received:', wakuMsg);
if (!wakuMsg.payload) return;
const directMessage: DirectMessage = decode(wakuMsg.payload);
// Only decrypt messages for us
if (!equalByteArrays(directMessage.toAddress, address)) return;
try {
const text = await decryptMessage(privateKey, directMessage);
const timestamp = wakuMsg.timestamp ? wakuMsg.timestamp : new Date();
console.log('Message decrypted:', text);
setter((prevMsgs: Message[]) => {
const copy = prevMsgs.slice();
copy.push({
text: text,
timestamp: timestamp,
});
return copy;
});
} catch (e) {
console.log(' Failed to decrypt message', e);
}
}