diff --git a/examples/eth-dm/src/App.tsx b/examples/eth-dm/src/App.tsx index d35887e871..011b7d01e0 100644 --- a/examples/eth-dm/src/App.tsx +++ b/examples/eth-dm/src/App.tsx @@ -10,7 +10,6 @@ import { decryptMessage, generateEthDmKeyPair, KeyPair, - recoverKeysFromPrivateKey, validatePublicKeyMessage, } from './crypto'; import { decode, DirectMessage, encode, PublicKeyMessage } from './messages'; @@ -18,29 +17,22 @@ import { Message, Messages } from './Messages'; import 'fontsource-roboto'; import { Button } from '@material-ui/core'; import { SendMessage } from './SendMessage'; +import { SaveKeyToStorage } from './SaveKeyToStorage'; +import { LoadKeyFromStorage } from './LoadKeyFromStorage'; export const PublicKeyContentTopic = '/eth-dm/1/public-key/json'; export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json'; -const EthDmKeyStorageKey = 'ethDmKey'; - declare let window: any; function App() { const [waku, setWaku] = useState(); const [provider, setProvider] = useState(); - const [ethDmKeyPair, setEthDmKeyPair] = useState( - retrieveKeysFromStorage - ); + const [ethDmKeyPair, setEthDmKeyPair] = useState(); const [publicKeyMsg, setPublicKeyMsg] = useState(); const [publicKeys, setPublicKeys] = useState>(new Map()); const [messages, setMessages] = useState([]); - useEffect(() => { - if (!ethDmKeyPair) return; - saveKeysToStorage(ethDmKeyPair); - }, [ethDmKeyPair]); - useEffect(() => { if (provider) return; try { @@ -66,7 +58,6 @@ function App() { const generateKeyPair = () => { if (ethDmKeyPair) return; - if (!provider) return; generateEthDmKeyPair() .then((keyPair) => { @@ -121,7 +112,7 @@ function App() { if (publicKeyMsg) { const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg); waku.lightPush.push(wakuMsg).catch((e) => { - console.error('Failed to send Public Key Message'); + console.error('Failed to send Public Key Message', e); }); } else { createPublicKeyMessage(provider.getSigner(), ethDmKeyPair.publicKey) @@ -129,7 +120,7 @@ function App() { setPublicKeyMsg(msg); const wakuMsg = encodePublicKeyWakuMessage(msg); waku.lightPush.push(wakuMsg).catch((e) => { - console.error('Failed to send Public Key Message'); + console.error('Failed to send Public Key Message', e); }); }) .catch((e) => { @@ -155,10 +146,33 @@ function App() { variant="contained" color="primary" onClick={generateKeyPair} - disabled={!provider || !!ethDmKeyPair} + disabled={!!ethDmKeyPair} > Generate Eth-DM Key Pair + +
+ setEthDmKeyPair(keyPair)} + disabled={!!ethDmKeyPair} + /> +
+
+ +
+
+
+ ); +} diff --git a/examples/eth-dm/src/SaveKeyToStorage.tsx b/examples/eth-dm/src/SaveKeyToStorage.tsx new file mode 100644 index 0000000000..eaaf4bbd90 --- /dev/null +++ b/examples/eth-dm/src/SaveKeyToStorage.tsx @@ -0,0 +1,53 @@ +import { Button, TextField } from '@material-ui/core'; +import React, { ChangeEvent, useState } from 'react'; +import { KeyPair } from './crypto'; +import { saveKeyPairToStorage } from './keyStorage'; + +export interface Props { + ethDmKeyPair: KeyPair | undefined; +} + +export function SaveKeyToStorage(props: Props) { + const [password, setPassword] = useState(); + + const ethDmKeyPair = props.ethDmKeyPair; + + const handlePasswordChange = (event: ChangeEvent) => { + setPassword(event.target.value); + }; + + 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/crypto.ts b/examples/eth-dm/src/crypto.ts index c4d87e8ee4..94535643a4 100644 --- a/examples/eth-dm/src/crypto.ts +++ b/examples/eth-dm/src/crypto.ts @@ -74,19 +74,6 @@ export function decryptMessage( return EthCrypto.decryptWithPrivateKey(privateKey, directMessage.encMessage); } -/** - * Recover Public Key and address from Private Key - */ -export function recoverKeysFromPrivateKey(privateKey: string) { - const publicKey = EthCrypto.publicKeyByPrivateKey(privateKey); - const address = EthCrypto.publicKey.toAddress(publicKey); - return { - privateKey, - publicKey, - address, - }; -} - /** * Encrypt message with given Public Key */ diff --git a/examples/eth-dm/src/keyStorage.ts b/examples/eth-dm/src/keyStorage.ts new file mode 100644 index 0000000000..5ed46de9f5 --- /dev/null +++ b/examples/eth-dm/src/keyStorage.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; + } +}