Merge pull request #216 from status-im/213-derivation

This commit is contained in:
Franck Royer 2021-06-30 11:14:26 +10:00 committed by GitHub
commit 90e39d3e0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 750 additions and 257 deletions

View File

@ -72,7 +72,8 @@
"wakuv",
"wakunode",
"webfonts",
"websockets"
"websockets",
"wifi"
],
"flagWords": [],
"ignorePaths": [

View File

@ -8,6 +8,7 @@
"version": "0.1.0",
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
@ -3451,6 +3452,28 @@
}
}
},
"node_modules/@material-ui/icons": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
"dependencies": {
"@babel/runtime": "^7.4.4"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"@material-ui/core": "^4.0.0",
"@types/react": "^16.8.6 || ^17.0.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@material-ui/styles": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
@ -26510,6 +26533,14 @@
"react-transition-group": "^4.4.0"
}
},
"@material-ui/icons": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
"requires": {
"@babel/runtime": "^7.4.4"
}
},
"@material-ui/styles": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",

View File

@ -5,6 +5,7 @@
"homepage": "/js-waku/eth-dm",
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",

View File

@ -1,40 +1,74 @@
import '@ethersproject/shims';
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import './App.css';
import { Environment, getStatusFleetNodes, Waku, WakuMessage } from 'js-waku';
import { Waku } from 'js-waku';
import { ethers } from 'ethers';
import { Web3Provider } from '@ethersproject/providers';
import {
createPublicKeyMessage,
generateEthDmKeyPair,
KeyPair,
validatePublicKeyMessage,
} from './crypto';
import * as EthCrypto from 'eth-crypto';
import { decode, DirectMessage, encode, PublicKeyMessage } from './messages';
import { Message, Messages } from './Messages';
import { KeyPair } from './crypto';
import { Message } from './messaging/Messages';
import 'fontsource-roboto';
import { Button } from '@material-ui/core';
import { SendMessage } from './SendMessage';
export const PublicKeyContentTopic = '/eth-dm/1/public-key/json';
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
import { AppBar, IconButton, Toolbar, Typography } from '@material-ui/core';
import KeyPairHandling from './key_pair_handling/KeyPairHandling';
import InitWaku from './InitWaku';
import {
createMuiTheme,
ThemeProvider,
makeStyles,
} from '@material-ui/core/styles';
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';
declare let window: any;
const theme = createMuiTheme({
palette: {
primary: {
main: purple[500],
},
secondary: {
main: teal[600],
},
},
});
const useStyles = makeStyles({
root: {
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
},
appBar: {
// height: '200p',
},
container: {
display: 'flex',
flex: 1,
},
main: {
flex: 1,
margin: '10px',
},
wakuStatus: {},
});
function App() {
const [waku, setWaku] = useState<Waku>();
const [provider, setProvider] = useState<Web3Provider>();
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair>();
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair | undefined>();
const [publicKeys, setPublicKeys] = useState<Map<string, string>>(new Map());
const [messages, setMessages] = useState<Message[]>([]);
const [address, setAddress] = useState<string>();
const classes = useStyles();
useEffect(() => {
if (provider) return;
try {
window.ethereum.enable();
window.ethereum.request({ method: 'eth_requestAccounts' });
const _provider = new ethers.providers.Web3Provider(window.ethereum);
setProvider(_provider);
} catch (e) {
@ -43,193 +77,66 @@ function App() {
}, [provider]);
useEffect(() => {
if (waku) return;
initWaku()
.then((wakuNode) => {
console.log('waku: ready');
setWaku(wakuNode);
})
.catch((e) => {
console.error('Failed to initiate Waku', e);
});
}, [waku]);
const generateKeyPair = () => {
if (ethDmKeyPair) return;
if (!provider) return;
generateEthDmKeyPair(provider.getSigner())
.then((keyPair) => {
setEthDmKeyPair(keyPair);
})
.catch((e) => {
console.error('Failed to generate Key Pair', e);
});
};
const observerPublicKeyMessage = handlePublicKeyMessage.bind(
{},
setPublicKeys
);
const observerDirectMessage = ethDmKeyPair
? handleDirectMessage.bind({}, setMessages, ethDmKeyPair.privateKey)
: undefined;
useEffect(() => {
if (!waku) return;
waku.relay.addObserver(observerPublicKeyMessage, [PublicKeyContentTopic]);
return function cleanUp() {
if (!waku) return;
waku.relay.deleteObserver(observerPublicKeyMessage, [
PublicKeyContentTopic,
]);
};
provider
?.getSigner()
.getAddress()
.then((address) => setAddress(address));
});
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,
]);
};
});
const broadcastPublicKey = () => {
if (!ethDmKeyPair) return;
if (!provider) return;
if (!waku) return;
if (publicKeyMsg) {
const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg);
waku.lightPush.push(wakuMsg).catch((e) => {
console.error('Failed to send Public Key Message');
});
} else {
createPublicKeyMessage(provider.getSigner(), ethDmKeyPair.publicKey)
.then((msg) => {
setPublicKeyMsg(msg);
const wakuMsg = encodePublicKeyWakuMessage(msg);
waku.lightPush.push(wakuMsg).catch((e) => {
console.error('Failed to send Public Key Message');
});
})
.catch((e) => {
console.error('Failed to creat Eth-Dm Publication message', e);
});
}
};
const wakuReady = !!waku ? 'Waku is ready' : 'Waku is loading';
return (
<div className="App">
<header className="App-header">
{wakuReady}
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
<Button
variant="contained"
color="primary"
onClick={generateKeyPair}
disabled={!provider}
>
Generate Eth-DM Key Pair
</Button>
<Button
variant="contained"
color="primary"
onClick={broadcastPublicKey}
disabled={!ethDmKeyPair || !waku}
>
Broadcast Eth-DM Public Key
</Button>
<ThemeProvider theme={theme}>
<div className={classes.root}>
<AppBar className={classes.appBar} position="static">
<Toolbar>
<Typography>Ethereum Direct Message</Typography>
<IconButton
edge="end"
className={classes.wakuStatus}
aria-label="waku-status"
>
<WifiIcon
color={waku ? undefined : 'disabled'}
style={waku ? { color: green[500] } : {}}
/>
</IconButton>
</Toolbar>
</AppBar>
<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
ethDmKeyPair={ethDmKeyPair}
setEthDmKeyPair={(keyPair) => setEthDmKeyPair(keyPair)}
/>
<BroadcastPublicKey
signer={provider?.getSigner()}
ethDmKeyPair={ethDmKeyPair}
waku={waku}
/>
</fieldset>
<fieldset>
<legend>Messaging</legend>
<Messaging
recipients={publicKeys}
waku={waku}
messages={messages}
/>
</fieldset>
</main>
</div>
<SendMessage recipients={publicKeys} waku={waku} />
<Messages messages={messages} />
</header>
</div>
</div>
</ThemeProvider>
);
}
export default App;
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() {
// Works with react-scripts
if (process?.env?.NODE_ENV === 'development') {
return getStatusFleetNodes(Environment.Test);
} else {
return getStatusFleetNodes(Environment.Prod);
}
}
function encodePublicKeyWakuMessage(ethDmMsg: PublicKeyMessage): WakuMessage {
const payload = encode(ethDmMsg);
return WakuMessage.fromBytes(payload, PublicKeyContentTopic);
}
function handlePublicKeyMessage(
setter: Dispatch<SetStateAction<Map<string, string>>>,
msg: WakuMessage
) {
if (!msg.payload) return;
const publicKeyMsg: PublicKeyMessage = decode(msg.payload);
const res = validatePublicKeyMessage(publicKeyMsg);
console.log(`Public Key Message Received, valid: ${res}`, publicKeyMsg);
setter((prevPks: Map<string, string>) => {
prevPks.set(publicKeyMsg.ethAddress, publicKeyMsg.ethDmPublicKey);
return new Map(prevPks);
});
}
async function handleDirectMessage(
setter: Dispatch<SetStateAction<Message[]>>,
privateKey: string,
wakuMsg: WakuMessage
) {
console.log('Waku Message received:', wakuMsg);
if (!wakuMsg.payload) return;
const directMessage: DirectMessage = decode(wakuMsg.payload);
const text = await EthCrypto.decryptWithPrivateKey(
privateKey,
directMessage.encMessage
);
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

@ -0,0 +1,62 @@
import { Button } from '@material-ui/core';
import React, { useState } from 'react';
import { createPublicKeyMessage, KeyPair } from './crypto';
import { encode, PublicKeyMessage } from './messaging/wire';
import { WakuMessage, Waku } from 'js-waku';
import { Signer } from '@ethersproject/abstract-signer';
import { PublicKeyContentTopic } from './InitWaku';
interface Props {
ethDmKeyPair: KeyPair | undefined;
waku: Waku | undefined;
signer: Signer | undefined;
}
export default function BroadcastPublicKey({
signer,
ethDmKeyPair,
waku,
}: Props) {
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
const broadcastPublicKey = () => {
if (!ethDmKeyPair) return;
if (!signer) return;
if (!waku) return;
if (publicKeyMsg) {
const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg);
waku.lightPush.push(wakuMsg).catch((e) => {
console.error('Failed to send Public Key Message', e);
});
} else {
createPublicKeyMessage(signer, ethDmKeyPair.publicKey)
.then((msg) => {
setPublicKeyMsg(msg);
const wakuMsg = encodePublicKeyWakuMessage(msg);
waku.lightPush.push(wakuMsg).catch((e) => {
console.error('Failed to send Public Key Message', e);
});
})
.catch((e) => {
console.error('Failed to create public key message', e);
});
}
};
return (
<Button
variant="contained"
color="primary"
onClick={broadcastPublicKey}
disabled={!ethDmKeyPair || !waku}
>
Broadcast Eth-DM Public Key
</Button>
);
}
function encodePublicKeyWakuMessage(ethDmMsg: PublicKeyMessage): WakuMessage {
const payload = encode(ethDmMsg);
return WakuMessage.fromBytes(payload, PublicKeyContentTopic);
}

View File

@ -0,0 +1,153 @@
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';
export const PublicKeyContentTopic = '/eth-dm/1/public-key/json';
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() {
// Works with react-scripts
if (process?.env?.NODE_ENV === 'development') {
return getStatusFleetNodes(Environment.Test);
} else {
return getStatusFleetNodes(Environment.Prod);
}
}
function handlePublicKeyMessage(
myPublicKey: string | undefined,
setter: Dispatch<SetStateAction<Map<string, string>>>,
msg: WakuMessage
) {
if (!msg.payload) return;
const publicKeyMsg: PublicKeyMessage = decode(msg.payload);
if (publicKeyMsg.ethDmPublicKey === myPublicKey) return;
const res = validatePublicKeyMessage(publicKeyMsg);
console.log(`Public Key Message Received, valid: ${res}`, publicKeyMsg);
setter((prevPks: Map<string, string>) => {
prevPks.set(publicKeyMsg.ethAddress, publicKeyMsg.ethDmPublicKey);
return new Map(prevPks);
});
}
async function handleDirectMessage(
setter: Dispatch<SetStateAction<Message[]>>,
privateKey: string,
address: string,
wakuMsg: WakuMessage
) {
console.log('Waku Message received:', wakuMsg);
if (!wakuMsg.payload) return;
const directMessage: DirectMessage = decode(wakuMsg.payload);
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

@ -1,30 +0,0 @@
export interface Message {
text: string;
timestamp: Date;
}
export interface Props {
messages: Message[];
}
export function Messages(props: Props) {
const messages = props.messages.map((msg) => {
return (
<li>
{formatDisplayDate(msg.timestamp)} {msg.text}
</li>
);
});
return <ul>{messages}</ul>;
}
function formatDisplayDate(timestamp: Date): string {
return timestamp.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
});
}

View File

@ -3,15 +3,11 @@ import '@ethersproject/shims';
import * as EthCrypto from 'eth-crypto';
import { ethers } from 'ethers';
import { Signer } from '@ethersproject/abstract-signer';
import { PublicKeyMessage } from './messages';
const Salt =
'Salt for Eth-Dm, do not share a signature of this message or others could decrypt your messages';
import { DirectMessage, PublicKeyMessage } from './messaging/wire';
export interface KeyPair {
privateKey: string;
publicKey: string;
address: string;
}
/**
@ -19,15 +15,8 @@ export interface KeyPair {
* the entropy for the EthCrypto keypair. Note that the entropy is hashed with keccak256
* to make the private key.
*/
export async function generateEthDmKeyPair(
web3Signer: Signer
): Promise<KeyPair> {
const signature = await web3Signer.signMessage(Salt);
// Need to remove '0x' prefix to allow buffer to decode the hex string.
const sigBuf = Buffer.from(signature.slice(2), 'hex');
const entropy = Buffer.concat([sigBuf, sigBuf]);
const keys = EthCrypto.createIdentity(entropy);
return keys;
export async function generateEthDmKeyPair(): Promise<KeyPair> {
return EthCrypto.createIdentity();
}
/**
@ -69,8 +58,24 @@ export function validatePublicKeyMessage(msg: PublicKeyMessage): boolean {
* context.
*/
function formatPublicKeyForSignature(ethDmPublicKey: string): string {
const txt = JSON.stringify({
return JSON.stringify({
ethDmPublicKey,
});
return txt;
}
/**
* Decrypt a Direct Message using the private key.
*/
export function decryptMessage(
privateKey: string,
directMessage: DirectMessage
) {
return EthCrypto.decryptWithPrivateKey(privateKey, directMessage.encMessage);
}
/**
* Encrypt message with given Public Key
*/
export async function encryptMessage(publicKey: string, message: string) {
return await EthCrypto.encryptWithPublicKey(publicKey, message);
}

View File

@ -0,0 +1,87 @@
import { Button } from '@material-ui/core';
import { LoadKeyPair } from './LoadKeyPair';
import { SaveKeyPair } from './SaveKeyPair';
import React, { useState } from 'react';
import { generateEthDmKeyPair, KeyPair } from '../crypto';
import { makeStyles } from '@material-ui/core/styles';
import PasswordInput from './PasswordInput';
const useStyles = makeStyles({
root: {
textAlign: 'center',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
margin: '5px',
},
generate: { margin: '5px' },
storage: {
margin: '5px',
},
loadSave: {
display: 'flex',
flexDirection: 'row',
margin: '5px',
},
loadSaveButton: {
margin: '5px',
},
});
export interface Props {
ethDmKeyPair: KeyPair | undefined;
setEthDmKeyPair: (keyPair: KeyPair) => void;
}
export default function KeyPairHandling({
ethDmKeyPair,
setEthDmKeyPair,
}: Props) {
const classes = useStyles();
const [password, setPassword] = useState<string>();
const generateKeyPair = () => {
if (ethDmKeyPair) return;
generateEthDmKeyPair()
.then((keyPair) => {
setEthDmKeyPair(keyPair);
})
.catch((e) => {
console.error('Failed to generate Key Pair', e);
});
};
return (
<div className={classes.root}>
<Button
className={classes.generate}
variant="contained"
color="primary"
onClick={generateKeyPair}
disabled={!!ethDmKeyPair}
>
Generate Eth-DM Key Pair
</Button>
<div className={classes.storage}>
<PasswordInput
password={password}
setPassword={(p) => setPassword(p)}
/>
<div className={classes.loadSave}>
<div className={classes.loadSaveButton}>
<LoadKeyPair
setEthDmKeyPair={(keyPair) => setEthDmKeyPair(keyPair)}
disabled={!!ethDmKeyPair}
password={password}
/>
</div>
<div className={classes.loadSaveButton}>
<SaveKeyPair ethDmKeyPair={ethDmKeyPair} password={password} />
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,33 @@
import { Button } from '@material-ui/core';
import React from 'react';
import { loadKeyPairFromStorage } from './key_pair_storage';
import { KeyPair } from '../crypto';
export interface Props {
setEthDmKeyPair: (keyPair: KeyPair) => void;
disabled: boolean;
password: string | undefined;
}
export function LoadKeyPair({ password, disabled, setEthDmKeyPair }: Props) {
const loadKeyPair = () => {
if (disabled) return;
if (!password) return;
loadKeyPairFromStorage(password).then((keyPair: KeyPair | undefined) => {
if (!keyPair) return;
console.log('EthDm KeyPair loaded from storage');
setEthDmKeyPair(keyPair);
});
};
return (
<Button
variant="contained"
color="primary"
onClick={loadKeyPair}
disabled={!password || disabled}
>
Load Eth-DM Key Pair from storage
</Button>
);
}

View File

@ -0,0 +1,24 @@
import { TextField } from '@material-ui/core';
import React, { ChangeEvent } from 'react';
interface Props {
password: string | undefined;
setPassword: (password: string) => void;
}
export default function PasswordInput({ password, setPassword }: Props) {
const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
};
return (
<TextField
id="password-input"
label="Password"
variant="filled"
type="password"
onChange={handlePasswordChange}
value={password}
/>
);
}

View File

@ -0,0 +1,30 @@
import { Button } from '@material-ui/core';
import React from 'react';
import { KeyPair } from '../crypto';
import { saveKeyPairToStorage } from './key_pair_storage';
export interface Props {
ethDmKeyPair: KeyPair | undefined;
password: string | undefined;
}
export function SaveKeyPair({ password, ethDmKeyPair }: Props) {
const saveKeyPair = () => {
if (!ethDmKeyPair) return;
if (!password) return;
saveKeyPairToStorage(ethDmKeyPair, password).then(() => {
console.log('EthDm KeyPair saved to storage');
});
};
return (
<Button
variant="contained"
color="primary"
onClick={saveKeyPair}
disabled={!password || !ethDmKeyPair}
>
Save Eth-DM Key Pair to storage
</Button>
);
}

View File

@ -0,0 +1,121 @@
import { KeyPair } from '../crypto';
/**
* Save keypair to storage, encrypted with password
*/
export async function saveKeyPairToStorage(
ethDmKeyPair: KeyPair,
password: string
) {
const { salt, iv, cipher } = await encryptKey(ethDmKeyPair, password);
const data = {
salt: new Buffer(salt).toString('hex'),
iv: new Buffer(iv).toString('hex'),
cipher: new Buffer(cipher).toString('hex'),
};
localStorage.setItem('cipherEthDmKeyPair', JSON.stringify(data));
}
/**
* Load keypair from storage, decrypted using password
*/
export async function loadKeyPairFromStorage(
password: string
): Promise<KeyPair | undefined> {
const str = localStorage.getItem('cipherEthDmKeyPair');
if (!str) return;
const data = JSON.parse(str);
const salt = new Buffer(data.salt, 'hex');
const iv = new Buffer(data.iv, 'hex');
const cipher = new Buffer(data.cipher, 'hex');
return await decryptKey(salt, iv, cipher, password);
}
/**
* Use password user as key material for wrap key.
*/
function getKeyMaterial(password: string): Promise<CryptoKey> {
const enc = new TextEncoder();
return window.crypto.subtle.importKey(
'raw',
enc.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
}
/**
* get key to store password
*/
function getWrapKey(keyMaterial: CryptoKey, salt: Uint8Array) {
return window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
}
/**
* Encrypt Eth-DM KeyPair using provided password
*/
async function encryptKey(ethDmKeyPair: KeyPair, password: string) {
const keyMaterial = await getKeyMaterial(password);
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const wrappingKey = await getWrapKey(keyMaterial, salt);
const enc = new TextEncoder();
const encodedKeyPair = enc.encode(JSON.stringify(ethDmKeyPair));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const cipher = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
wrappingKey,
encodedKeyPair
);
return { salt, iv, cipher };
}
/**
* Derive a key from a password, and use the key to decrypt the cipher key pair.
*/
async function decryptKey(
salt: Buffer,
iv: Buffer,
cipherKeyPair: Buffer,
password: string
): Promise<KeyPair | undefined> {
const keyMaterial = await getKeyMaterial(password);
const key = await getWrapKey(keyMaterial, salt);
try {
let decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
cipherKeyPair
);
let dec = new TextDecoder();
return JSON.parse(dec.decode(decrypted));
} catch (e) {
return;
}
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import { List, ListItem, ListItemText } from '@material-ui/core';
/**
* Clear text message
*/
export interface Message {
text: string;
timestamp: Date;
}
export interface Props {
messages: Message[];
}
export default function Messages({ messages }: Props) {
return <List dense={true}>{generate(messages)}</List>;
}
function generate(messages: Message[]) {
return messages.map((msg) => {
const text = `<${formatDisplayDate(msg.timestamp)}> ${msg.text}`;
return (
<ListItem>
<ListItemText key={formatDisplayDate(msg.timestamp)} primary={text} />
</ListItem>
);
});
}
function formatDisplayDate(timestamp: Date): string {
return timestamp.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
});
}

View File

@ -0,0 +1,30 @@
import Messages, { Message } from './Messages';
import { Waku } from 'js-waku';
import SendMessage from './SendMessage';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
root: {
display: 'flex',
alignItems: 'left',
flexDirection: 'column',
margin: '5px',
},
});
interface Props {
waku: Waku | undefined;
recipients: Map<string, string>;
messages: Message[];
}
export default function Messaging({ waku, recipients, messages }: Props) {
const classes = useStyles();
return (
<div className={classes.root}>
<SendMessage recipients={recipients} waku={waku} />
<Messages messages={messages} />
</div>
);
}

View File

@ -8,9 +8,9 @@ import {
} from '@material-ui/core';
import React, { ChangeEvent, useState, KeyboardEvent } from 'react';
import { Waku, WakuMessage } from 'js-waku';
import * as EthCrypto from 'eth-crypto';
import { DirectMessage, encode } from './messages';
import { DirectMessageContentTopic } from './App';
import { DirectMessage, encode } from './wire';
import { encryptMessage } from '../crypto';
import { DirectMessageContentTopic } from '../InitWaku';
const useStyles = makeStyles((theme) => ({
formControl: {
@ -28,13 +28,11 @@ export interface Props {
recipients: Map<string, string>;
}
export function SendMessage(props: Props) {
export default function SendMessage({ waku, recipients }: Props) {
const classes = useStyles();
const [recipient, setRecipient] = useState<string>('');
const [message, setMessage] = useState<string>();
const waku = props.waku;
const handleRecipientChange = (
event: ChangeEvent<{ name?: string; value: unknown }>
) => {
@ -45,7 +43,7 @@ export function SendMessage(props: Props) {
setMessage(event.target.value);
};
const items = Array.from(props.recipients.keys()).map((recipient) => {
const items = Array.from(recipients.keys()).map((recipient) => {
return (
<MenuItem key={recipient} value={recipient}>
{recipient}
@ -63,7 +61,7 @@ export function SendMessage(props: Props) {
if (!waku) return;
if (!recipient) return;
if (!message) return;
const publicKey = props.recipients.get(recipient);
const publicKey = recipients.get(recipient);
if (!publicKey) return;
sendMessage(waku, recipient, publicKey, message, (res) => {
@ -111,7 +109,7 @@ async function encodeEncryptedWakuMessage(
publicKey: string,
address: string
): Promise<WakuMessage> {
const encryptedMsg = await EthCrypto.encryptWithPublicKey(publicKey, message);
const encryptedMsg = await encryptMessage(publicKey, message);
const directMsg: DirectMessage = {
toAddress: address,