mirror of
https://github.com/morten-olsen/catpic.delivery.git
synced 2026-02-08 01:46:26 +01:00
init
This commit is contained in:
20
src/App.tsx
Normal file
20
src/App.tsx
Normal 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;
|
||||
54
src/containers/Connected.tsx
Normal file
54
src/containers/Connected.tsx
Normal 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;
|
||||
45
src/containers/Welcome.tsx
Normal file
45
src/containers/Welcome.tsx
Normal 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;
|
||||
126
src/contexts/ConnectionContext.tsx
Normal file
126
src/contexts/ConnectionContext.tsx
Normal 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;
|
||||
11
src/hooks/useConnection.ts
Normal file
11
src/hooks/useConnection.ts
Normal 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
22
src/hooks/useCrypto.ts
Normal 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
13
src/index.tsx
Normal 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
92
src/utils/crypto.ts
Normal 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 "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user