mirror of https://github.com/waku-org/js-waku.git
Encrypt KeyPair before saving to storage
This commit is contained in:
parent
47a27a0969
commit
9a68cc2a86
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue