diff --git a/.cspell.json b/.cspell.json index c77fadba33..e32f92c4da 100644 --- a/.cspell.json +++ b/.cspell.json @@ -72,7 +72,8 @@ "wakuv", "wakunode", "webfonts", - "websockets" + "websockets", + "wifi" ], "flagWords": [], "ignorePaths": [ diff --git a/examples/eth-dm/package-lock.json b/examples/eth-dm/package-lock.json index 875cb74f35..18ca7dca48 100644 --- a/examples/eth-dm/package-lock.json +++ b/examples/eth-dm/package-lock.json @@ -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", diff --git a/examples/eth-dm/package.json b/examples/eth-dm/package.json index 4851ff1e55..1c7cb88c86 100644 --- a/examples/eth-dm/package.json +++ b/examples/eth-dm/package.json @@ -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", diff --git a/examples/eth-dm/src/App.tsx b/examples/eth-dm/src/App.tsx index dcad23c0e3..b375f0cfbc 100644 --- a/examples/eth-dm/src/App.tsx +++ b/examples/eth-dm/src/App.tsx @@ -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(); const [provider, setProvider] = useState(); - const [ethDmKeyPair, setEthDmKeyPair] = useState(); - const [publicKeyMsg, setPublicKeyMsg] = useState(); + const [ethDmKeyPair, setEthDmKeyPair] = useState(); const [publicKeys, setPublicKeys] = useState>(new Map()); const [messages, setMessages] = useState([]); + const [address, setAddress] = useState(); + + 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 ( -
-
- {wakuReady} -
- - + +
+ + + Ethereum Direct Message + + + + + + +
+
+ +
+ Eth-DM Key Pair + setEthDmKeyPair(keyPair)} + /> + +
+
+ Messaging + +
+
- - -
-
+ + ); } export default App; - -async function initWaku(): Promise { - 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>>, - 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) => { - prevPks.set(publicKeyMsg.ethAddress, publicKeyMsg.ethDmPublicKey); - return new Map(prevPks); - }); -} - -async function handleDirectMessage( - setter: Dispatch>, - 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; - }); -} diff --git a/examples/eth-dm/src/BroadcastPublicKey.tsx b/examples/eth-dm/src/BroadcastPublicKey.tsx new file mode 100644 index 0000000000..f73e4cf9a4 --- /dev/null +++ b/examples/eth-dm/src/BroadcastPublicKey.tsx @@ -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(); + + 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 ( + + ); +} + +function encodePublicKeyWakuMessage(ethDmMsg: PublicKeyMessage): WakuMessage { + const payload = encode(ethDmMsg); + return WakuMessage.fromBytes(payload, PublicKeyContentTopic); +} diff --git a/examples/eth-dm/src/InitWaku.tsx b/examples/eth-dm/src/InitWaku.tsx new file mode 100644 index 0000000000..ad1a56b649 --- /dev/null +++ b/examples/eth-dm/src/InitWaku.tsx @@ -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>>; + setMessages: Dispatch>; + 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 { + 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>>, + 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) => { + prevPks.set(publicKeyMsg.ethAddress, publicKeyMsg.ethDmPublicKey); + return new Map(prevPks); + }); +} + +async function handleDirectMessage( + setter: Dispatch>, + 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; + }); +} diff --git a/examples/eth-dm/src/Messages.tsx b/examples/eth-dm/src/Messages.tsx deleted file mode 100644 index d12756f91a..0000000000 --- a/examples/eth-dm/src/Messages.tsx +++ /dev/null @@ -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 ( -
  • - {formatDisplayDate(msg.timestamp)} {msg.text} -
  • - ); - }); - - return
      {messages}
    ; -} - -function formatDisplayDate(timestamp: Date): string { - return timestamp.toLocaleString([], { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: false, - }); -} diff --git a/examples/eth-dm/src/crypto.ts b/examples/eth-dm/src/crypto.ts index 08a32f8732..b357efcfef 100644 --- a/examples/eth-dm/src/crypto.ts +++ b/examples/eth-dm/src/crypto.ts @@ -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 { - 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 { + 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); } diff --git a/examples/eth-dm/src/key_pair_handling/KeyPairHandling.tsx b/examples/eth-dm/src/key_pair_handling/KeyPairHandling.tsx new file mode 100644 index 0000000000..a9db833941 --- /dev/null +++ b/examples/eth-dm/src/key_pair_handling/KeyPairHandling.tsx @@ -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(); + + const generateKeyPair = () => { + if (ethDmKeyPair) return; + + generateEthDmKeyPair() + .then((keyPair) => { + setEthDmKeyPair(keyPair); + }) + .catch((e) => { + console.error('Failed to generate Key Pair', e); + }); + }; + + return ( +
    + +
    + setPassword(p)} + /> +
    +
    + setEthDmKeyPair(keyPair)} + disabled={!!ethDmKeyPair} + password={password} + /> +
    +
    + +
    +
    +
    +
    + ); +} diff --git a/examples/eth-dm/src/key_pair_handling/LoadKeyPair.tsx b/examples/eth-dm/src/key_pair_handling/LoadKeyPair.tsx new file mode 100644 index 0000000000..2a79cb37c0 --- /dev/null +++ b/examples/eth-dm/src/key_pair_handling/LoadKeyPair.tsx @@ -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 ( + + ); +} diff --git a/examples/eth-dm/src/key_pair_handling/PasswordInput.tsx b/examples/eth-dm/src/key_pair_handling/PasswordInput.tsx new file mode 100644 index 0000000000..cc0e770555 --- /dev/null +++ b/examples/eth-dm/src/key_pair_handling/PasswordInput.tsx @@ -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) => { + setPassword(event.target.value); + }; + + return ( + + ); +} diff --git a/examples/eth-dm/src/key_pair_handling/SaveKeyPair.tsx b/examples/eth-dm/src/key_pair_handling/SaveKeyPair.tsx new file mode 100644 index 0000000000..f8fef3399a --- /dev/null +++ b/examples/eth-dm/src/key_pair_handling/SaveKeyPair.tsx @@ -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 ( + + ); +} diff --git a/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts b/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts new file mode 100644 index 0000000000..d6fc7d2da6 --- /dev/null +++ b/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts @@ -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 { + 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 { + 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 { + 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; + } +} diff --git a/examples/eth-dm/src/messaging/Messages.tsx b/examples/eth-dm/src/messaging/Messages.tsx new file mode 100644 index 0000000000..2c3c1f54d8 --- /dev/null +++ b/examples/eth-dm/src/messaging/Messages.tsx @@ -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 {generate(messages)}; +} + +function generate(messages: Message[]) { + return messages.map((msg) => { + const text = `<${formatDisplayDate(msg.timestamp)}> ${msg.text}`; + + return ( + + + + ); + }); +} + +function formatDisplayDate(timestamp: Date): string { + return timestamp.toLocaleString([], { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: false, + }); +} diff --git a/examples/eth-dm/src/messaging/Messaging.tsx b/examples/eth-dm/src/messaging/Messaging.tsx new file mode 100644 index 0000000000..60d07ff98b --- /dev/null +++ b/examples/eth-dm/src/messaging/Messaging.tsx @@ -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; + messages: Message[]; +} + +export default function Messaging({ waku, recipients, messages }: Props) { + const classes = useStyles(); + + return ( +
    + + +
    + ); +} diff --git a/examples/eth-dm/src/SendMessage.tsx b/examples/eth-dm/src/messaging/SendMessage.tsx similarity index 88% rename from examples/eth-dm/src/SendMessage.tsx rename to examples/eth-dm/src/messaging/SendMessage.tsx index f35bd2c4de..9057c21df9 100644 --- a/examples/eth-dm/src/SendMessage.tsx +++ b/examples/eth-dm/src/messaging/SendMessage.tsx @@ -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; } -export function SendMessage(props: Props) { +export default function SendMessage({ waku, recipients }: Props) { const classes = useStyles(); const [recipient, setRecipient] = useState(''); const [message, setMessage] = useState(); - 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 ( {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 { - const encryptedMsg = await EthCrypto.encryptWithPublicKey(publicKey, message); + const encryptedMsg = await encryptMessage(publicKey, message); const directMsg: DirectMessage = { toAddress: address, diff --git a/examples/eth-dm/src/messages.ts b/examples/eth-dm/src/messaging/wire.ts similarity index 100% rename from examples/eth-dm/src/messages.ts rename to examples/eth-dm/src/messaging/wire.ts