Encrypt KeyPair before saving to storage

This commit is contained in:
Franck Royer 2021-06-28 15:09:32 +10:00
parent 47a27a0969
commit 9a68cc2a86
No known key found for this signature in database
GPG Key ID: A82ED75A8DFC50A4
5 changed files with 259 additions and 41 deletions

View File

@ -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<Waku>();
const [provider, setProvider] = useState<Web3Provider>();
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair | undefined>(
retrieveKeysFromStorage
);
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair | undefined>();
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
const [publicKeys, setPublicKeys] = useState<Map<string, string>>(new Map());
const [messages, setMessages] = useState<Message[]>([]);
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
</Button>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
<LoadKeyFromStorage
setEthDmKeyPair={(keyPair) => setEthDmKeyPair(keyPair)}
disabled={!!ethDmKeyPair}
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
<SaveKeyToStorage ethDmKeyPair={ethDmKeyPair} />
</div>
<div>
<Button
variant="contained"
color="primary"
@ -243,16 +257,3 @@ async function handleDirectMessage(
return copy;
});
}
function saveKeysToStorage(ethDmKeyPair: KeyPair) {
// /!\ Bad idea to store keys in clear. At least put a password on it.
localStorage.setItem(EthDmKeyStorageKey, ethDmKeyPair.privateKey);
}
function retrieveKeysFromStorage() {
const privateKey = window.localStorage.getItem(EthDmKeyStorageKey);
if (privateKey) {
return recoverKeysFromPrivateKey(privateKey);
}
return;
}

View File

@ -0,0 +1,56 @@
import { Button, TextField } from '@material-ui/core';
import React, { ChangeEvent, useState } from 'react';
import { loadKeyPairFromStorage } from './keyStorage';
import { KeyPair } from './crypto';
export interface Props {
setEthDmKeyPair: (keyPair: KeyPair) => void;
disabled: boolean;
}
export function LoadKeyFromStorage(props: Props) {
const [password, setPassword] = useState<string>();
const disabled = props.disabled;
const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
};
const loadKeyPair = () => {
if (disabled) return;
if (!password) return;
loadKeyPairFromStorage(password).then((keyPair: KeyPair | undefined) => {
if (!keyPair) return;
console.log('EthDm KeyPair loaded from storage');
props.setEthDmKeyPair(keyPair);
});
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
<TextField
id="password-input"
label="Password"
variant="filled"
type="password"
onChange={handlePasswordChange}
value={password}
/>
<Button
variant="contained"
color="primary"
onClick={loadKeyPair}
disabled={!password || disabled}
>
Load Eth-DM Key Pair from storage
</Button>
</div>
);
}

View File

@ -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<string>();
const ethDmKeyPair = props.ethDmKeyPair;
const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
};
const saveKeyPair = () => {
if (!ethDmKeyPair) return;
if (!password) return;
saveKeyPairToStorage(ethDmKeyPair, password).then(() => {
console.log('EthDm KeyPair saved to storage');
});
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
<TextField
id="password-input"
label="Password"
variant="filled"
type="password"
onChange={handlePasswordChange}
value={password}
/>
<Button
variant="contained"
color="primary"
onClick={saveKeyPair}
disabled={!password || !ethDmKeyPair}
>
Save Eth-DM Key Pair to storage
</Button>
</div>
);
}

View File

@ -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
*/

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;
}
}