This commit is contained in:
Morten Olsen
2021-06-09 00:03:41 +02:00
commit b1b24f9774
14 changed files with 5990 additions and 0 deletions

20
src/App.tsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react';
import Welcome from './containers/Welcome';
import Connected from './containers/Connected';
import useConnection, { ConnectionStates } from './hooks/useConnection';
const App: React.FC<{}> = () => {
const { state } = useConnection();
if (state === ConnectionStates.WAITING) {
return <Welcome />
}
if (state === ConnectionStates.CONNECTED) {
return <Connected />
}
return (
<div>Connected</div>
);
};
export default App;

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import useConnection from '../hooks/useConnection';
const readFile = (file: File) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve({
name: file.name,
type: file.type,
body: reader.result,
});
};
reader.onerror = (err) => {
reject(err);
};
reader.readAsDataURL(file);
});
const Connected: React.FC<{}> = () => {
const { send, messages } = useConnection();
const onDrop = useCallback(async (acceptedFiles: File[]) => {
const files = await Promise.all(acceptedFiles.map(readFile));
files.forEach(send);
}, [send]);
const {
getRootProps,
getInputProps,
isDragActive,
} = useDropzone({onDrop})
return (
<>
<div {...getRootProps()}>
<input {...getInputProps()} />
{ isDragActive ? (
<p>Drop the files here ...</p>
):(
<p>Drag 'n' drop some files here, or click to select files</p>
)}
</div>
{messages.map((message) => (
<div>
{message.name}-{message.body.length}
<img style={{ width: 300, height: 300 }} src={message.body} />
</div>
))}
</>
);
}
export default Connected;

View File

@@ -0,0 +1,45 @@
import React, { useCallback, useEffect } from 'react';
import QRCode from 'react-qr-code';
import QRReader from 'react-qr-reader'
import useConnection from '../hooks/useConnection';
const Welcome: React.FC<{}> = () => {
const { connect, clientInfo } = useConnection();
const onScan = useCallback(
(result) => {
if (result) {
connect(JSON.parse(result));
}
},
[],
);
useEffect(() => {
const hash = window.location.hash;
if (hash) {
const clientInfoEncoded = hash.substring(1);
const clientInfo = JSON.parse(atob(clientInfoEncoded));
connect(clientInfo);
}
console.log(hash);
}, []);
return (
<>
<div>{location.protocol}//{location.host}{location.pathname}#{btoa(JSON.stringify(clientInfo))}</div>
<QRCode
value={JSON.stringify(clientInfo)}
size={300}
/>
<QRReader
delay={300}
onScan={onScan}
onError={(result) => { console.error(result) }}
style={{ width: '300px', height: '300px' }}
/>
</>
);
}
export default Welcome;

View File

@@ -0,0 +1,126 @@
import React, { createContext, useMemo, useState, useCallback, useEffect } from 'react';
import Peer, { DataConnection } from 'peerjs';
import { nanoid } from 'nanoid';
import useCrypto from '../hooks/useCrypto';
enum States {
WAITING,
CONNECTING,
CONNECTED,
}
interface ConnectionContextValue {
clientInfo: any;
state: States;
messages: any[];
send: (message: any) => Promise<void>;
connect: (connectionInfo: any) => Promise<void>;
}
interface Message {
id: string;
packages: number;
content: string;
}
function dataURItoBlob(dataURI: string) {
var mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
var binary = atob(dataURI.split(',')[1]);
var array = [];
for (var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
const blob = new Blob([new Uint8Array(array)], {type: mime});
return URL.createObjectURL(blob);
}
const ConnectionContext = createContext<ConnectionContextValue>(undefined as any);
const ConnectionProvider: React.FC = ({ children }) => {
const [secret, setSecret] = useState(nanoid());
const { encrypt, decrypt } = useCrypto(secret);
const id = useMemo(() => nanoid(), []);
const peer = useMemo(() => new Peer(id), [id]);
const [connection, setConnection] = useState<DataConnection | undefined>(undefined);
const [messages, setMessages] = useState<Message[]>([]);
const [state, setState] = useState<States>(States.WAITING);
const clientInfo = useMemo(() => ({
id,
secret,
}), [id]);
const send = useCallback(async (message: any) => {
if (!connection) return;
setMessages(current => [
...current,
{
...message,
body: dataURItoBlob(message.body),
},
]);
connection.send(await encrypt(message));
}, [connection, encrypt]);
const connect = useCallback(async (clientInfo: any) => {
setState(States.CONNECTING);
const newConnection = peer.connect(clientInfo.id);
newConnection.on('open', () => {
setSecret(clientInfo.secret);
setState(States.CONNECTED);
setConnection(newConnection);
console.log('connected', newConnection);
});
}, [peer]);
useEffect(() => {
const onConnect = (newConnection: DataConnection) => {
setState(States.CONNECTED);
setConnection(newConnection);
console.log('connected', newConnection);
};
peer.on('connection', onConnect);
return () => {
peer.off('connection', onConnect);
};
}, [peer]);
useEffect(() => {
if (!connection) {
return;
}
const handleData = async (encrypted: any) => {
const message = await decrypt(encrypted);
setMessages(current => [
...current,
{
...message,
body: dataURItoBlob(message.body),
},
]);
};
connection.on('data', handleData);
return () => {
connection.off('data', handleData);
}
}, [connection, decrypt]);
return (
<ConnectionContext.Provider
value={{
clientInfo,
state,
messages,
send,
connect,
}}
>
{children}
</ConnectionContext.Provider>
);
};
export { States, ConnectionProvider };
export default ConnectionContext;

View File

@@ -0,0 +1,11 @@
import { useContext } from 'react';
import ConnectionContext, { States } from '../contexts/ConnectionContext';
const useConnection = () => {
const context = useContext(ConnectionContext);
return context;
};
export const ConnectionStates = States;
export default useConnection;

22
src/hooks/useCrypto.ts Normal file
View File

@@ -0,0 +1,22 @@
import { useCallback } from 'react';
import { encrypt, decrypt } from '../utils/crypto';
const useCrypto = (secret: string) => {
const doEncrypt = useCallback(async (data: any) => {
const raw = JSON.stringify(data);
const result = await encrypt(raw, secret);
return result;
}, [secret]);
const doDecrypt = useCallback(async (data: string[]) => {
return decrypt(data, secret);
}, [secret]);
return {
encrypt: doEncrypt,
decrypt: doDecrypt,
};
}
export default useCrypto;

13
src/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import { render } from 'react-dom';
import { ConnectionProvider } from './contexts/ConnectionContext';
import App from './App';
const root = document.getElementById('root');
const app = (
<ConnectionProvider>
<App />
</ConnectionProvider>
);
render(app, root);

92
src/utils/crypto.ts Normal file
View File

@@ -0,0 +1,92 @@
function chunkSubstr(str: string, size: number) {
const numChunks = Math.ceil(str.length / size)
const chunks = new Array(numChunks)
for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
chunks[i] = str.substr(o, size)
}
return chunks
}
const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff));
const base64_to_buf = (b64) =>
Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));
const enc = new TextEncoder();
const dec = new TextDecoder();
const getPasswordKey = (password: string) =>
window.crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, [
"deriveKey",
]);
const deriveKey = (passwordKey: CryptoKey, salt: Uint8Array, keyUsage: KeyUsage[]) =>
window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 50000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
keyUsage
);
export async function encrypt(secretData: string, password: string) {
const parts = chunkSubstr(secretData, 1000000);
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const passwordKey = await getPasswordKey(password);
const resultParts = await Promise.all(parts.map(async (part) => {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const aesKey = await deriveKey(passwordKey, salt, ["encrypt"]);
const encryptedContent = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
aesKey,
enc.encode(part)
);
const encryptedContentArr = new Uint8Array(encryptedContent);
let buff = new Uint8Array(
salt.byteLength + iv.byteLength + encryptedContentArr.byteLength
);
buff.set(salt, 0);
buff.set(iv, salt.byteLength);
buff.set(encryptedContentArr, salt.byteLength + iv.byteLength);
//const base64Buff = buff_to_base64(buff);
return buff;
}));
return resultParts;
}
export async function decrypt(encryptedData: Uint8Array[], password: string) {
try {
const passwordKey = await getPasswordKey(password);
const parts = await Promise.all(encryptedData.map(async (part) => {
const encryptedDataBuff = part;
const salt = encryptedDataBuff.slice(0, 16);
const iv = encryptedDataBuff.slice(16, 16 + 12);
const data = encryptedDataBuff.slice(16 + 12);
const aesKey = await deriveKey(passwordKey, salt, ["decrypt"]);
const decryptedContent = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
aesKey,
data
);
return dec.decode(decryptedContent);
}));
return JSON.parse(parts.join(''));
} catch (e) {
console.log(`Error - ${e}`);
return "";
}
}