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,
|
decryptMessage,
|
||||||
generateEthDmKeyPair,
|
generateEthDmKeyPair,
|
||||||
KeyPair,
|
KeyPair,
|
||||||
recoverKeysFromPrivateKey,
|
|
||||||
validatePublicKeyMessage,
|
validatePublicKeyMessage,
|
||||||
} from './crypto';
|
} from './crypto';
|
||||||
import { decode, DirectMessage, encode, PublicKeyMessage } from './messages';
|
import { decode, DirectMessage, encode, PublicKeyMessage } from './messages';
|
||||||
|
@ -18,29 +17,22 @@ import { Message, Messages } from './Messages';
|
||||||
import 'fontsource-roboto';
|
import 'fontsource-roboto';
|
||||||
import { Button } from '@material-ui/core';
|
import { Button } from '@material-ui/core';
|
||||||
import { SendMessage } from './SendMessage';
|
import { SendMessage } from './SendMessage';
|
||||||
|
import { SaveKeyToStorage } from './SaveKeyToStorage';
|
||||||
|
import { LoadKeyFromStorage } from './LoadKeyFromStorage';
|
||||||
|
|
||||||
export const PublicKeyContentTopic = '/eth-dm/1/public-key/json';
|
export const PublicKeyContentTopic = '/eth-dm/1/public-key/json';
|
||||||
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
|
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
|
||||||
|
|
||||||
const EthDmKeyStorageKey = 'ethDmKey';
|
|
||||||
|
|
||||||
declare let window: any;
|
declare let window: any;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [waku, setWaku] = useState<Waku>();
|
const [waku, setWaku] = useState<Waku>();
|
||||||
const [provider, setProvider] = useState<Web3Provider>();
|
const [provider, setProvider] = useState<Web3Provider>();
|
||||||
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair | undefined>(
|
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair | undefined>();
|
||||||
retrieveKeysFromStorage
|
|
||||||
);
|
|
||||||
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
|
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
|
||||||
const [publicKeys, setPublicKeys] = useState<Map<string, string>>(new Map());
|
const [publicKeys, setPublicKeys] = useState<Map<string, string>>(new Map());
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ethDmKeyPair) return;
|
|
||||||
saveKeysToStorage(ethDmKeyPair);
|
|
||||||
}, [ethDmKeyPair]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (provider) return;
|
if (provider) return;
|
||||||
try {
|
try {
|
||||||
|
@ -66,7 +58,6 @@ function App() {
|
||||||
|
|
||||||
const generateKeyPair = () => {
|
const generateKeyPair = () => {
|
||||||
if (ethDmKeyPair) return;
|
if (ethDmKeyPair) return;
|
||||||
if (!provider) return;
|
|
||||||
|
|
||||||
generateEthDmKeyPair()
|
generateEthDmKeyPair()
|
||||||
.then((keyPair) => {
|
.then((keyPair) => {
|
||||||
|
@ -121,7 +112,7 @@ function App() {
|
||||||
if (publicKeyMsg) {
|
if (publicKeyMsg) {
|
||||||
const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg);
|
const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg);
|
||||||
waku.lightPush.push(wakuMsg).catch((e) => {
|
waku.lightPush.push(wakuMsg).catch((e) => {
|
||||||
console.error('Failed to send Public Key Message');
|
console.error('Failed to send Public Key Message', e);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
createPublicKeyMessage(provider.getSigner(), ethDmKeyPair.publicKey)
|
createPublicKeyMessage(provider.getSigner(), ethDmKeyPair.publicKey)
|
||||||
|
@ -129,7 +120,7 @@ function App() {
|
||||||
setPublicKeyMsg(msg);
|
setPublicKeyMsg(msg);
|
||||||
const wakuMsg = encodePublicKeyWakuMessage(msg);
|
const wakuMsg = encodePublicKeyWakuMessage(msg);
|
||||||
waku.lightPush.push(wakuMsg).catch((e) => {
|
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) => {
|
.catch((e) => {
|
||||||
|
@ -155,10 +146,33 @@ function App() {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={generateKeyPair}
|
onClick={generateKeyPair}
|
||||||
disabled={!provider || !!ethDmKeyPair}
|
disabled={!!ethDmKeyPair}
|
||||||
>
|
>
|
||||||
Generate Eth-DM Key Pair
|
Generate Eth-DM Key Pair
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -243,16 +257,3 @@ async function handleDirectMessage(
|
||||||
return copy;
|
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);
|
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
|
* 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