Merge pull request #76 from waku-org/remove-eth-pm-wallet

This commit is contained in:
fryorcraken.eth 2022-08-29 16:57:35 +10:00 committed by GitHub
commit b403ca574c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 0 additions and 14346 deletions

View File

@ -13,7 +13,6 @@ jobs:
matrix:
example:
[
eth-pm-wallet-encryption,
eth-pm,
relay-angular-chat,
relay-reactjs-chat,

View File

@ -1,3 +0,0 @@
# Remove ReactJS warning about webpack
# because this is not a monorepo, ReactJS projects are examples
SKIP_PREFLIGHT_CHECK=true

View File

@ -1,24 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/cache/

View File

@ -1,36 +0,0 @@
# Ethereum Private Message Using Wallet Encryption Web App
**Demonstrates**:
- Private Messaging
- React/TypeScript
- Waku Light Push
- Signature with Web3 using [EIP-712: `eth_signTypedData_v4`](https://eips.ethereum.org/EIPS/eip-712)
- Asymmetric Encryption
- Usage of [`eth_decrypt`](https://docs.metamask.io/guide/rpc-api.html#eth-decrypt) Wallet API
This dApp demonstrates how to send and received end-to-end encrypted messages
using the encryption API provided by some Web3 Wallet provider such as [MetaMask](https://metamask.io/).
The sender only needs to know the Ethereum address of the recipient.
The recipient must broadcast his encryption public key as a first step.
The `master` branch's HEAD is deployed at https://js-waku.wakuconnect.dev/examples/eth-pm-wallet-encryption/.
To run a development version locally, do:
```shell
git clone https://github.com/status-im/js-waku/ ; cd js-waku
npm install # Install dependencies for js-waku
npm run build # Build js-waku
cd examples/eth-pm-wallet-encryption
npm install # Install dependencies for the web app
npm run start # Start development server to serve the web app on http://localhost:3000/js-waku/eth-pm-wallet
```
## Caveats
This is a PoC with some obvious UX caveats:
- As the message payload is fully encrypted, the dApp asks MetaMask who in turns ask the user to decrypt every received message (even if we are the sender).
- This only uses Relay protocol to receive messages, meaning that participants must have the dApp open at the same time to receive private messages or public keys from each other.

View File

@ -1,60 +0,0 @@
const webpack = require('webpack');
module.exports = {
dev: (config) => {
// Override webpack 5 config from react-scripts to load polyfills
if (!config.resolve) config.resolve = {};
if (!config.resolve.fallback) config.resolve.fallback = {};
Object.assign(config.resolve.fallback, {
buffer: require.resolve('buffer'),
crypto: false,
stream: require.resolve('stream-browserify'),
});
if (!config.plugins) config.plugins = [];
config.plugins.push(
new webpack.DefinePlugin({
'process.env.ENV': JSON.stringify('dev'),
})
);
config.plugins.push(
new webpack.ProvidePlugin({
process: 'process/browser.js',
Buffer: ['buffer', 'Buffer'],
})
);
if (!config.ignoreWarnings) config.ignoreWarnings = [];
config.ignoreWarnings.push(/Failed to parse source map/);
return config;
},
prod: (config) => {
// Override webpack 5 config from react-scripts to load polyfills
if (!config.resolve) config.resolve = {};
if (!config.resolve.fallback) config.resolve.fallback = {};
Object.assign(config.resolve.fallback, {
buffer: require.resolve('buffer'),
crypto: false,
stream: require.resolve('stream-browserify'),
});
if (!config.plugins) config.plugins = [];
config.plugins.push(
new webpack.DefinePlugin({
'process.env.ENV': JSON.stringify('prod'),
})
);
config.plugins.push(
new webpack.ProvidePlugin({
process: 'process/browser.js',
Buffer: ['buffer', 'Buffer'],
})
);
if (!config.ignoreWarnings) config.ignoreWarnings = [];
config.ignoreWarnings.push(/Failed to parse source map/);
return config;
},
};

View File

@ -1,73 +0,0 @@
{
"name": "eth-pm-wallet-encryption",
"version": "0.1.0",
"private": true,
"homepage": "/examples/eth-pm-wallet-encryption",
"dependencies": {
"@ethersproject/providers": "^5.6.8",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^13.2.0",
"@testing-library/user-event": "^14.1.0",
"@types/jest": "^27.4.0",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"eth-sig-util": "^3.0.1",
"ethers": "^5.5.4",
"fontsource-roboto": "^4.0.0",
"js-waku": "^0.24.0",
"protobufjs": "^6.11.2",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"uint8arrays": "^3.1.0"
},
"scripts": {
"start": "cra-webpack-rewired start",
"build": "run-s build:*",
"build:react": "cra-webpack-rewired build",
"eject": "cra-webpack-rewired eject",
"fix": "run-s fix:*",
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts --ext .tsx",
"test:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --list-different",
"test:spelling": "cspell \"{README.md,src/**/*.{ts,tsx},public/**/*.html}\" -c ../.cspell.json",
"fix:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --write",
"fix:lint": "eslint src --ext .ts --ext .tsx --fix"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not ie <= 99",
"not android <= 4.4.4",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@ethersproject/shims": "^5.5.0",
"@types/node": "^17.0.17",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"cra-webpack-rewired": "^1.0.1",
"cspell": "^6.0.0",
"eslint": "^8.9.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.5.1",
"process": "^0.11.10",
"react-scripts": "5.0.0",
"stream-browserify": "^3.0.0",
"typescript": "^4.5.5"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #dddddd;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: black;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,231 +0,0 @@
import "@ethersproject/shims";
import React, { useEffect, useState } from "react";
import "./App.css";
import { Waku } from "js-waku";
import { Message } from "./messaging/Messages";
import "fontsource-roboto";
import { AppBar, IconButton, Toolbar, Typography } from "@material-ui/core";
import {
createMuiTheme,
ThemeProvider,
makeStyles,
} from "@material-ui/core/styles";
import { lightBlue, orange, teal } from "@material-ui/core/colors";
import WifiIcon from "@material-ui/icons/Wifi";
import BroadcastPublicKey from "./BroadcastPublicKey";
import Messaging from "./messaging/Messaging";
import {
PrivateMessageContentTopic,
handlePrivateMessage,
handlePublicKeyMessage,
initWaku,
PublicKeyContentTopic,
} from "./waku";
import { Web3Provider } from "@ethersproject/providers/src.ts/web3-provider";
import GetEncryptionPublicKey from "./GetEncryptionPublicKey";
import ConnectWallet from "./ConnectWallet";
const theme = createMuiTheme({
palette: {
primary: {
main: orange[500],
},
secondary: {
main: lightBlue[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: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
peers: {},
});
function App() {
const [waku, setWaku] = useState<Waku>();
const [provider, setProvider] = useState<Web3Provider>();
const [encPublicKey, setEncPublicKey] = useState<Uint8Array>();
const [publicKeys, setPublicKeys] = useState<Map<string, Uint8Array>>(
new Map()
);
const [messages, setMessages] = useState<Message[]>([]);
const [address, setAddress] = useState<string>();
const [peerStats, setPeerStats] = useState<{
relayPeers: number;
lightPushPeers: number;
}>({
relayPeers: 0,
lightPushPeers: 0,
});
const classes = useStyles();
// 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;
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 (!address) return;
if (!provider?.provider?.request) return;
const observerPrivateMessage = handlePrivateMessage.bind(
{},
setMessages,
address,
provider.provider.request
);
waku.relay.addObserver(observerPrivateMessage, [
PrivateMessageContentTopic,
]);
return function cleanUp() {
if (!waku) return;
if (!observerPrivateMessage) return;
waku.relay.deleteObserver(observerPrivateMessage, [
PrivateMessageContentTopic,
]);
};
}, [waku, address, provider?.provider?.request]);
useEffect(() => {
if (!waku) return;
const interval = setInterval(async () => {
let lightPushPeers = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _peer of waku.store.peers) {
lightPushPeers++;
}
setPeerStats({
relayPeers: waku.relay.getPeers().size,
lightPushPeers,
});
}, 1000);
return () => clearInterval(interval);
}, [waku]);
let addressDisplay = "";
if (address) {
addressDisplay =
address.substr(0, 6) + "..." + address.substr(address.length - 4, 4);
}
return (
<ThemeProvider theme={theme}>
<div className={classes.root}>
<AppBar className={classes.appBar} position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.wakuStatus}
aria-label="waku-status"
>
<WifiIcon
color={waku ? undefined : "disabled"}
style={waku ? { color: teal[500] } : {}}
/>
</IconButton>
<Typography className={classes.peers} aria-label="connected-peers">
Peers: {peerStats.relayPeers} relay, {peerStats.lightPushPeers}{" "}
light push
</Typography>
<Typography variant="h6" className={classes.title}>
Ethereum Private Message with Wallet Encryption
</Typography>
<Typography>{addressDisplay}</Typography>
</Toolbar>
</AppBar>
<div className={classes.container}>
<main className={classes.main}>
<fieldset>
<legend>Wallet</legend>
<ConnectWallet
setProvider={setProvider}
setAddress={setAddress}
/>
</fieldset>
<fieldset>
<legend>Encryption Keys</legend>
<GetEncryptionPublicKey
setEncPublicKey={setEncPublicKey}
providerRequest={provider?.provider?.request}
address={address}
/>
<BroadcastPublicKey
address={address}
encryptionPublicKey={encPublicKey}
waku={waku}
providerRequest={provider?.provider?.request}
/>
</fieldset>
<fieldset>
<legend>Messaging</legend>
<Messaging
recipients={publicKeys}
waku={waku}
messages={messages}
/>
</fieldset>
</main>
</div>
</div>
</ThemeProvider>
);
}
export default App;

View File

@ -1,69 +0,0 @@
import { Button } from "@material-ui/core";
import React from "react";
import { createPublicKeyMessage } from "./crypto";
import { PublicKeyMessage } from "./messaging/wire";
import { WakuMessage, Waku } from "js-waku";
import { PublicKeyContentTopic } from "./waku";
interface Props {
encryptionPublicKey: Uint8Array | undefined;
waku: Waku | undefined;
address: string | undefined;
providerRequest:
| ((request: { method: string; params?: Array<any> }) => Promise<any>)
| undefined;
}
export default function BroadcastPublicKey({
encryptionPublicKey,
address,
waku,
providerRequest,
}: Props) {
const broadcastPublicKey = () => {
if (!encryptionPublicKey) return;
if (!address) return;
if (!waku) return;
if (!providerRequest) return;
console.log("Creating Public Key Message");
createPublicKeyMessage(address, encryptionPublicKey, providerRequest)
.then((msg) => {
console.log("Public Key Message created");
encodePublicKeyWakuMessage(msg)
.then((wakuMsg) => {
console.log("Public Key Message encoded");
waku.lightPush
.push(wakuMsg)
.then((res) => console.log("Public Key Message pushed", res))
.catch((e) => {
console.error("Failed to send Public Key Message", e);
});
})
.catch(() => {
console.log("Failed to encode Public Key Message in Waku Message");
});
})
.catch((e) => {
console.error("Failed to create public key message", e);
});
};
return (
<Button
variant="contained"
color="primary"
onClick={broadcastPublicKey}
disabled={!encryptionPublicKey || !waku || !address || !providerRequest}
>
Broadcast Encryption Public Key
</Button>
);
}
async function encodePublicKeyWakuMessage(
publicKeyMessage: PublicKeyMessage
): Promise<WakuMessage> {
const payload = publicKeyMessage.encode();
return await WakuMessage.fromBytes(payload, PublicKeyContentTopic);
}

View File

@ -1,33 +0,0 @@
import { Button } from "@material-ui/core";
import React from "react";
import { ethers } from "ethers";
import { Web3Provider } from "@ethersproject/providers/src.ts/web3-provider";
declare let window: any;
interface Props {
setAddress: (address: string) => void;
setProvider: (provider: Web3Provider) => void;
}
export default function ConnectWallet({ setAddress, setProvider }: Props) {
const connectWallet = () => {
try {
window.ethereum
.request({ method: "eth_requestAccounts" })
.then((accounts: string[]) => {
const _provider = new ethers.providers.Web3Provider(window.ethereum);
setAddress(accounts[0]);
setProvider(_provider);
});
} catch (e) {
console.error("No web3 provider available");
}
};
return (
<Button variant="contained" color="primary" onClick={connectWallet}>
Connect Wallet
</Button>
);
}

View File

@ -1,56 +0,0 @@
import { Button } from "@material-ui/core";
import React from "react";
interface Props {
setEncPublicKey: (key: Uint8Array) => void;
providerRequest:
| ((request: { method: string; params?: Array<any> }) => Promise<any>)
| undefined;
address: string | undefined;
}
export default function GetEncryptionPublicKey({
setEncPublicKey,
providerRequest,
address,
}: Props) {
const requestPublicKey = () => {
if (!providerRequest) return;
if (!address) return;
console.log("Getting Encryption Public Key from Wallet");
providerRequest({
method: "eth_getEncryptionPublicKey",
params: [address],
})
.then((key: string | undefined) => {
console.log("Encryption Public key:", key);
if (typeof key !== "string") {
console.error("Could not get encryption key");
return;
}
setEncPublicKey(Buffer.from(key, "base64"));
})
.catch((error) => {
if (error.code === 4001) {
// EIP-1193 userRejectedRequest error
console.log("We can't encrypt anything without the key.");
} else {
console.error(error);
}
});
};
return (
<Button
variant="contained"
color="primary"
onClick={requestPublicKey}
disabled={!providerRequest || !address}
>
Get Encryption Public Key from Wallet
</Button>
);
}

View File

@ -1,105 +0,0 @@
import "@ethersproject/shims";
import { PublicKeyMessage } from "./messaging/wire";
import { utils } from "js-waku";
import * as sigUtil from "eth-sig-util";
import { equals } from "uint8arrays/equals";
/**
* Sign the encryption public key with Web3. This can then be published to let other
* users know to use this encryption public key to encrypt messages for the
* Ethereum Address holder.
*/
export async function createPublicKeyMessage(
address: string,
encryptionPublicKey: Uint8Array,
providerRequest: (request: {
method: string;
params?: Array<any>;
}) => Promise<any>
): Promise<PublicKeyMessage> {
const signature = await signEncryptionKey(
encryptionPublicKey,
address,
providerRequest
);
console.log("Asking wallet to sign Public Key Message");
console.log("Public Key Message signed");
return new PublicKeyMessage({
encryptionPublicKey: encryptionPublicKey,
ethAddress: utils.hexToBytes(address),
signature: utils.hexToBytes(signature),
});
}
function buildMsgParams(encryptionPublicKey: Uint8Array, fromAddress: string) {
return JSON.stringify({
domain: {
name: "Ethereum Private Message over Waku",
version: "1",
},
message: {
message:
"By signing this message you certify that messages addressed to `ownerAddress` must be encrypted with `encryptionPublicKey`",
encryptionPublicKey: utils.bytesToHex(encryptionPublicKey),
ownerAddress: fromAddress,
},
// Refers to the keys of the *types* object below.
primaryType: "PublishEncryptionPublicKey",
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
],
PublishEncryptionPublicKey: [
{ name: "message", type: "string" },
{ name: "encryptionPublicKey", type: "string" },
{ name: "ownerAddress", type: "string" },
],
},
});
}
export async function signEncryptionKey(
encryptionPublicKey: Uint8Array,
fromAddress: string,
providerRequest: (request: {
method: string;
params?: Array<any>;
from?: string;
}) => Promise<any>
): Promise<Uint8Array> {
const msgParams = buildMsgParams(encryptionPublicKey, fromAddress);
const result = await providerRequest({
method: "eth_signTypedData_v4",
params: [fromAddress, msgParams],
from: fromAddress,
});
console.log("TYPED SIGNED:" + JSON.stringify(result));
return utils.hexToBytes(result);
}
/**
* Validate that the Encryption Public Key was signed by the holder of the given Ethereum address.
*/
export function validatePublicKeyMessage(msg: PublicKeyMessage): boolean {
const recovered = sigUtil.recoverTypedSignature_v4({
data: JSON.parse(
buildMsgParams(
msg.encryptionPublicKey,
"0x" + utils.bytesToHex(msg.ethAddress)
)
),
sig: "0x" + utils.bytesToHex(msg.signature),
});
console.log("Recovered", recovered);
console.log("ethAddress", "0x" + utils.bytesToHex(msg.ethAddress));
return equals(utils.hexToBytes(recovered), msg.ethAddress);
}

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,11 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,40 +0,0 @@
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

@ -1,30 +0,0 @@
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, Uint8Array>;
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

@ -1,154 +0,0 @@
import {
FormControl,
InputLabel,
makeStyles,
MenuItem,
Select,
TextField,
} from "@material-ui/core";
import React, { ChangeEvent, useState, KeyboardEvent } from "react";
import { utils, Waku, WakuMessage } from "js-waku";
import { PrivateMessage } from "./wire";
import { PrivateMessageContentTopic } from "../waku";
import * as sigUtil from "eth-sig-util";
const useStyles = makeStyles((theme) => ({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}));
export interface Props {
waku: Waku | undefined;
// address, public key
recipients: Map<string, Uint8Array>;
}
export default function SendMessage({ waku, recipients }: Props) {
const classes = useStyles();
const [recipient, setRecipient] = useState<string>("");
const [message, setMessage] = useState<string>();
const handleRecipientChange = (
event: ChangeEvent<{ name?: string; value: unknown }>
) => {
setRecipient(event.target.value as string);
};
const handleMessageChange = (event: ChangeEvent<HTMLInputElement>) => {
setMessage(event.target.value);
};
const items = Array.from(recipients.keys()).map((recipient) => {
return (
<MenuItem key={recipient} value={recipient}>
{recipient}
</MenuItem>
);
});
const keyDownHandler = async (event: KeyboardEvent<HTMLInputElement>) => {
if (
event.key === "Enter" &&
!event.altKey &&
!event.ctrlKey &&
!event.shiftKey
) {
if (!waku) return;
if (!recipient) return;
if (!message) return;
const publicKey = recipients.get(recipient);
if (!publicKey) return;
sendMessage(waku, recipient, publicKey, message, (res) => {
if (res) {
console.log("callback called with", res);
setMessage("");
}
});
}
};
return (
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
}}
>
<FormControl className={classes.formControl}>
<InputLabel id="select-recipient-label">Recipient</InputLabel>
<Select
labelId="select-recipient"
id="select-recipient"
value={recipient}
onChange={handleRecipientChange}
>
{items}
</Select>
</FormControl>
<TextField
id="message-input"
label="Message"
variant="filled"
onChange={handleMessageChange}
onKeyDown={keyDownHandler}
value={message}
/>
</div>
);
}
async function encodeEncryptedWakuMessage(
message: string,
publicKey: Uint8Array,
address: string
): Promise<WakuMessage> {
const privateMessage = new PrivateMessage({
toAddress: utils.hexToBytes(address),
message: message,
});
const payload = privateMessage.encode();
const encObj = sigUtil.encrypt(
Buffer.from(publicKey).toString("base64"),
{ data: utils.bytesToHex(payload) },
"x25519-xsalsa20-poly1305"
);
const encryptedPayload = Buffer.from(JSON.stringify(encObj), "utf8");
return WakuMessage.fromBytes(encryptedPayload, PrivateMessageContentTopic);
}
function sendMessage(
waku: Waku,
recipientAddress: string,
recipientPublicKey: Uint8Array,
message: string,
callback: (res: boolean) => void
) {
encodeEncryptedWakuMessage(message, recipientPublicKey, recipientAddress)
.then((msg) => {
console.log("pushing");
waku.lightPush
.push(msg)
.then((res) => {
console.log("Message sent", res);
callback(res ? res.isSuccess : false);
})
.catch((e) => {
console.error("Failed to send message", e);
callback(false);
});
})
.catch((e) => {
console.error("Cannot encode & encrypt message", e);
callback(false);
});
}

View File

@ -1,101 +0,0 @@
import * as protobuf from "protobufjs/light";
export interface PublicKeyMessagePayload {
encryptionPublicKey: Uint8Array;
ethAddress: Uint8Array;
signature: Uint8Array;
}
const Root = protobuf.Root,
Type = protobuf.Type,
Field = protobuf.Field;
/**
* Message used to communicate the encryption public key linked to a given Ethereum account
*/
export class PublicKeyMessage {
private static Type = new Type("PublicKeyMessage")
.add(new Field("encryptionPublicKey", 1, "bytes"))
.add(new Field("ethAddress", 2, "bytes"))
.add(new Field("signature", 3, "bytes"));
private static Root = new Root()
.define("messages")
.add(PublicKeyMessage.Type);
constructor(public payload: PublicKeyMessagePayload) {}
public encode(): Uint8Array {
const message = PublicKeyMessage.Type.create(this.payload);
return PublicKeyMessage.Type.encode(message).finish();
}
public static decode(
bytes: Uint8Array | Buffer
): PublicKeyMessage | undefined {
const payload = PublicKeyMessage.Type.decode(
bytes
) as unknown as PublicKeyMessagePayload;
if (
!payload.signature ||
!payload.encryptionPublicKey ||
!payload.ethAddress
) {
console.log("Field missing on decoded Public Key Message", payload);
return;
}
return new PublicKeyMessage(payload);
}
get encryptionPublicKey(): Uint8Array {
return this.payload.encryptionPublicKey;
}
get ethAddress(): Uint8Array {
return this.payload.ethAddress;
}
get signature(): Uint8Array {
return this.payload.signature;
}
}
export interface PrivateMessagePayload {
toAddress: Uint8Array;
message: string;
}
/**
* Encrypted Message used for private communication over the Waku network.
*/
export class PrivateMessage {
private static Type = new Type("PrivateMessage")
.add(new Field("toAddress", 1, "bytes"))
.add(new Field("message", 2, "string"));
private static Root = new Root().define("messages").add(PrivateMessage.Type);
constructor(public payload: PrivateMessagePayload) {}
public encode(): Uint8Array {
const message = PrivateMessage.Type.create(this.payload);
return PrivateMessage.Type.encode(message).finish();
}
public static decode(bytes: Uint8Array | Buffer): PrivateMessage | undefined {
const payload = PrivateMessage.Type.decode(
bytes
) as unknown as PrivateMessagePayload;
if (!payload.toAddress || !payload.message) {
console.log("Field missing on decoded PrivateMessage", payload);
return;
}
return new PrivateMessage(payload);
}
get toAddress(): Uint8Array {
return this.payload.toAddress;
}
get message(): string {
return this.payload.message;
}
}

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";

View File

@ -1,94 +0,0 @@
import { Dispatch, SetStateAction } from "react";
import { utils, Waku, WakuMessage } from "js-waku";
import { PrivateMessage, PublicKeyMessage } from "./messaging/wire";
import { validatePublicKeyMessage } from "./crypto";
import { Message } from "./messaging/Messages";
import { equals } from "uint8arrays/equals";
export const PublicKeyContentTopic =
"/eth-pm-wallet/1/encryption-public-key/proto";
export const PrivateMessageContentTopic =
"/eth-pm-wallet/1/private-message/proto";
export async function initWaku(): Promise<Waku> {
const waku = await Waku.create({ bootstrap: { default: true } });
// 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 | undefined,
setPublicKeys: Dispatch<SetStateAction<Map<string, Uint8Array>>>,
msg: WakuMessage
) {
console.log("Public Key Message received:", msg);
if (!msg.payload) return;
const publicKeyMsg = PublicKeyMessage.decode(msg.payload);
if (!publicKeyMsg) return;
if (myAddress && equals(publicKeyMsg.ethAddress, utils.hexToBytes(myAddress)))
return;
const res = validatePublicKeyMessage(publicKeyMsg);
console.log("Is Public Key Message valid?", res);
if (res) {
setPublicKeys((prevPks: Map<string, Uint8Array>) => {
prevPks.set(
utils.bytesToHex(publicKeyMsg.ethAddress),
publicKeyMsg.encryptionPublicKey
);
return new Map(prevPks);
});
}
}
export async function handlePrivateMessage(
setter: Dispatch<SetStateAction<Message[]>>,
address: string,
providerRequest: (request: {
method: string;
params?: Array<any>;
}) => Promise<any>,
wakuMsg: WakuMessage
) {
console.log("Private Message received:", wakuMsg);
if (!wakuMsg.payload) return;
const decryptedPayload = await providerRequest({
method: "eth_decrypt",
params: [wakuMsg.payloadAsUtf8, address],
}).catch((error) => console.log(error.message));
console.log("Decrypted Payload:", decryptedPayload);
const privateMessage = PrivateMessage.decode(
Buffer.from(decryptedPayload, "hex")
);
if (!privateMessage) {
console.log("Failed to decode Private Message");
return;
}
if (!equals(privateMessage.toAddress, utils.hexToBytes(address))) return;
const timestamp = wakuMsg.timestamp ? wakuMsg.timestamp : new Date();
console.log("Message decrypted:", privateMessage.message);
setter((prevMsgs: Message[]) => {
const copy = prevMsgs.slice();
copy.push({
text: privateMessage.message,
timestamp: timestamp,
});
return copy;
});
}

View File

@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}