This commit is contained in:
2020-08-22 14:56:41 +02:00
parent 79d89f266a
commit 26028445bf
16 changed files with 315 additions and 176 deletions

View File

@@ -3,16 +3,19 @@ import { hot } from 'react-hot-loader/root';
import { Layout } from 'antd'; import { Layout } from 'antd';
import { GithubProvider } from './contexts/Github'; import { GithubProvider } from './contexts/Github';
import { EncryptionProvider } from './contexts/Encryption'; import { EncryptionProvider } from './contexts/Encryption';
import { DecryptionProvider } from './contexts/Decryption';
import AppRouter from './Router'; import AppRouter from './Router';
const App: React.FC = () => ( const App: React.FC = () => (
<GithubProvider> <GithubProvider>
<EncryptionProvider> <EncryptionProvider>
<Layout style={{minHeight:"100vh"}}> <DecryptionProvider>
<Layout.Content style={{ padding: '25px', maxWidth: '800px', width: '100%', margin: 'auto' }}> <Layout style={{minHeight:"100vh"}}>
<AppRouter/> <Layout.Content style={{ padding: '25px', maxWidth: '800px', width: '100%', margin: 'auto' }}>
</Layout.Content> <AppRouter/>
</Layout> </Layout.Content>
</Layout>
</DecryptionProvider>
</EncryptionProvider> </EncryptionProvider>
</GithubProvider> </GithubProvider>
); );

View File

@@ -6,6 +6,7 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import Encrypt from './screens/Encrypt'; import Encrypt from './screens/Encrypt';
import Decrypt from './screens/Decrypt';
import Welcome from './screens/Welcome'; import Welcome from './screens/Welcome';
import Debug from './screens/Debug'; import Debug from './screens/Debug';
@@ -18,6 +19,9 @@ const AppRouter: React.FC = () => (
<Route path="/welcome"> <Route path="/welcome">
<Welcome /> <Welcome />
</Route> </Route>
<Route path="/decrypt">
<Decrypt />
</Route>
<Route path="/"> <Route path="/">
<Encrypt /> <Encrypt />
</Route> </Route>

View File

@@ -1,8 +1,7 @@
import React, {useMemo} from 'react'; import React from 'react';
import { import {
List, List,
Button, Button,
Tooltip,
Popconfirm, Popconfirm,
} from 'antd'; } from 'antd';
import { import {
@@ -11,9 +10,8 @@ import {
IssuesCloseOutlined, IssuesCloseOutlined,
LockOutlined, LockOutlined,
DownloadOutlined, DownloadOutlined,
ShareAltOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { FileType } from '../contexts/Encryption'; import FileType from '../types/File';
import { downloadLink } from '../helpers/files'; import { downloadLink } from '../helpers/files';
interface Props { interface Props {
@@ -22,22 +20,11 @@ interface Props {
} }
const icons: {[name: string]: any} = { const icons: {[name: string]: any} = {
encrypting: <SyncOutlined spin />, processing: <SyncOutlined spin />,
failed: <IssuesCloseOutlined />, failed: <IssuesCloseOutlined />,
encrypted: <LockOutlined />, success: <LockOutlined />,
}; };
const share = async (file: FileType, fileData: File[]) => {
try {
navigator.share({
title: file.name,
files: fileData,
} as any);
} catch (err) {
alert(err);
}
}
const IconText = ({ icon, text, ...props }) => ( const IconText = ({ icon, text, ...props }) => (
<Button <Button
{...props} {...props}
@@ -50,13 +37,9 @@ const FileView: React.FC<Props> = ({
remove, remove,
}) => { }) => {
const icon = icons[file.status]; const icon = icons[file.status];
const fileData = useMemo(() => [new File([file.link || ''], file.name, {
type: 'text/plain',
})], [file]);
const actions = []; const actions = [];
if (file.link) { if (file.blob) {
actions.push( actions.push(
<Popconfirm <Popconfirm
title="Are you sure delete this file?" title="Are you sure delete this file?"
@@ -73,23 +56,13 @@ const FileView: React.FC<Props> = ({
); );
} }
if (!!navigator.share && (navigator as any).canSare && (navigator as any).canShare({ files: fileData })) { if (file.blob) {
actions.push(
<IconText
icon={ShareAltOutlined}
text="Share"
onClick={() => share(file, fileData)}
/>
);
}
if (file.link) {
actions.push( actions.push(
<IconText <IconText
icon={DownloadOutlined} icon={DownloadOutlined}
type="primary" type="primary"
text="Download" text="Download"
onClick={() => downloadLink(file.name, file.link!)} onClick={() => downloadLink(file.name, file.blob!)}
/> />
); );
} }
@@ -101,7 +74,6 @@ const FileView: React.FC<Props> = ({
<List.Item.Meta <List.Item.Meta
avatar={icon} avatar={icon}
title={file.name} title={file.name}
description={`Encrypted for ${file.reciever}`}
/> />
</List.Item> </List.Item>
); );

View File

@@ -1,12 +1,19 @@
import React, { useContext } from 'react'; import React from 'react';
import { Space, List, Empty, Button } from 'antd'; import { Space, List, Empty, Button } from 'antd';
import { DownloadOutlined } from '@ant-design/icons'; import { DownloadOutlined } from '@ant-design/icons';
import EncryptionContext from '../contexts/Encryption';
import useDownloadAll from '../hooks/useDownloadAll'; import useDownloadAll from '../hooks/useDownloadAll';
import File from './File'; import File from './File';
import FileType from '../types/File';
const Encrypt: React.FC = () => { interface Props {
const { files, deleteFile } = useContext(EncryptionContext); files: {[id: string]: FileType};
deleteFile: (id: string) => void;
}
const Encrypt: React.FC<Props> = ({
files,
deleteFile,
}) => {
const { status, downloadAll } = useDownloadAll(); const { status, downloadAll } = useDownloadAll();
if (Object.keys(files).length === 0) { if (Object.keys(files).length === 0) {

View File

@@ -1,58 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { Theme } from '../theme';
interface Props {
left?: React.ReactNode;
right?: React.ReactNode;
title?: string;
body?: string;
children?: React.ReactNode;
}
const Cell = styled.div<{ theme: Theme }>`
padding: ${({ theme }) => theme.margin.medium / 2}px;
align-items: center;
`;
const Wrapper = styled(Cell)`
display: flex;
flex-direction: row;
background: #dfe6e9;
margin-top: ${({ theme }) => theme.margin.medium}px;
`;
const Title = styled.h2`
padding: 0px;
font-size: 22px;
font-weight: bold;
`
const Main = styled(Cell)`
flex: 1;
justify-content: flex-start;
`;
const Row: React.FC<Props> = ({
left,
right,
title,
body,
children,
}) => (
<Wrapper style={{ display: 'flex' }}>
{left}
<Main>
{title && <Title>{title}</Title>}
{body}
{children}
</Main>
{right}
</Wrapper>
);
export {
Cell,
};
export default Row;

View File

@@ -0,0 +1,36 @@
import React, { useContext, useCallback } from 'react';
import styled from 'styled-components';
import { Layout } from 'antd';
import { FileAddTwoTone } from '@ant-design/icons';
import { useDropzone } from 'react-dropzone';
import DecryptionContext from '../../contexts/Decryption';
const Icon = styled(FileAddTwoTone)`
font-size: 100px;
margin-bottom: 20px;
`;
const DropWrapper = styled(Layout)`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 50px;
`;
const AddFile: React.FC = () => {
const { addFile } = useContext(DecryptionContext);
const onDrop = useCallback(acceptedFiles => {
acceptedFiles.forEach(addFile);
}, [])
const {getRootProps, getInputProps} = useDropzone({ onDrop });
return (
<DropWrapper {...getRootProps()}>
<input {...getInputProps()} />
<Icon />
<p>Drag 'n' drop some files here, or click to select files</p>
</DropWrapper>
);
};
export default AddFile;

View File

@@ -3,7 +3,7 @@ import styled from 'styled-components';
import { Layout } from 'antd'; import { Layout } from 'antd';
import { FileAddTwoTone } from '@ant-design/icons'; import { FileAddTwoTone } from '@ant-design/icons';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import EncryptionContext from '../contexts/Encryption'; import EncryptionContext from '../../contexts/Encryption';
const Icon = styled(FileAddTwoTone)` const Icon = styled(FileAddTwoTone)`
font-size: 100px; font-size: 100px;

View File

@@ -1,7 +1,7 @@
import React, { useState, useCallback, useContext } from 'react'; import React, { useState, useCallback, useContext } from 'react';
import { Input, Form, Button } from 'antd'; import { Input, Form, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import EncryptionContext from '../contexts/Encryption'; import EncryptionContext from '../../contexts/Encryption';
const AddText : React.FC = () => { const AddText : React.FC = () => {
const { addText } = useContext(EncryptionContext); const { addText } = useContext(EncryptionContext);

106
src/contexts/Decryption.tsx Normal file
View File

@@ -0,0 +1,106 @@
import React, { useState, useCallback, useContext, createContext, useEffect } from 'react';
import * as openpgp from 'openpgp';
import GithubContext from './Github';
import FileType from '../types/File';
import { createFile } from '../helpers/files';
interface DecryptionContextType {
publicKey: string | undefined;
files: {[id: string]: FileType};
addFile: (file: File) => Promise<void>;
deleteFile: (id: string) => void;
}
const DecryptionContext = createContext<DecryptionContextType>({
publicKey: undefined,
files: {},
addFile: async () => { throw new Error('Not using provider'); },
deleteFile: async () => { throw new Error('Not using provider'); },
});
const decrypt = async (privateKey: string, keys: string[], content: string) => {
const armoredKeys = await Promise.all(keys.map(openpgp.key.readArmored));
const message = openpgp.message.fromText(content);
const encrypted = await openpgp.decrypt({
message,
privateKeys: [...(await openpgp.key.readArmored(privateKey)).keys],
publicKeys: armoredKeys.reduce<any>((output, key: any) => [...output, ...key.keys], []),
});
const { data } = encrypted;
const blob = new Blob([data], {
type: 'text/text',
});
return blob;
};
const DecryptionProvider: React.FC = ({
children,
}) => {
const { username, keys } = useContext(GithubContext);
const [privateKey, setPrivateKey] = useState<string | undefined>(undefined);
const [publicKey, setPublicKey] = useState<string | undefined>(undefined);
const [files, setFiles] = useState<DecryptionContextType['files']>({});
const deleteFile = useCallback((id: string) => {
delete files[id];
setFiles({
...files,
});
}, [files]);
useEffect(() => {
const run = async () => {
const currentRawKey = localStorage.getItem('key');
if (currentRawKey) {
setPrivateKey(currentRawKey);
const key = await openpgp.key.readArmored(currentRawKey);
setPublicKey(key.keys[0].toPublic().armor());
} else {
const key = await openpgp.generateKey({
userIds: [{ name: 'unknown unknown', email: 'unknown@unknown.foo'}],
curve: 'ed25519',
});
setPrivateKey(key.privateKeyArmored);
setPublicKey(key.publicKeyArmored);
localStorage.setItem('key', key.privateKeyArmored);
}
};
run();
}, []);
const addFile = useCallback(async (file: File) => {
if (!keys || !privateKey) return;
const addedFile = createFile(setFiles, file.name);
const reader = new FileReader()
reader.onabort = addedFile.setFailed,
reader.onerror = addedFile.setFailed,
reader.onload = () => {
addedFile.setContent(
decrypt(privateKey, keys, reader.result as string),
);
}
reader.readAsText(file)
}, [keys, username]);
return (
<DecryptionContext.Provider
value={{
publicKey,
files,
addFile,
deleteFile,
}}
>
{children}
</DecryptionContext.Provider>
);
};
export {
DecryptionProvider,
};
export default DecryptionContext;;

View File

@@ -1,16 +1,8 @@
import React, { useState, useCallback, useContext, createContext } from 'react'; import React, { useState, useCallback, useContext, createContext } from 'react';
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
import { nanoid } from 'nanoid';
import { message } from 'antd';
import GithubContext from './Github'; import GithubContext from './Github';
import { createFile } from '../helpers/files';
export interface FileType { import FileType from '../types/File';
name: string;
reciever: string;
status: 'encrypting' | 'failed' | 'encrypted';
error?: any;
link?: Blob;
}
interface EncryptionContextType { interface EncryptionContextType {
files: {[id: string]: FileType}; files: {[id: string]: FileType};
@@ -55,74 +47,27 @@ const EncryptionProvider: React.FC = ({
}); });
}, [files]); }, [files]);
const add = (name: string) => {
const id = nanoid();
const file: FileType = {
name: `${name}.asc`,
reciever: username,
status: 'encrypting',
};
setFiles(files => ({
...files,
[id]: file,
}));
const setError = (err: any) => {
console.error(err);
setFiles(files => ({
...files,
[id]: {
...files[id],
status: 'failed',
error: err,
},
}));
message.error(`Failed to encrypt ${name}`);
};
const setContent = (text: string, keys: string[]) => {
const run = async () => {
try {
const encrypted = await encrypt(keys, text);
setFiles(files => ({
...files,
[id]: {
...files[id],
link: encrypted,
status: 'encrypted'
},
}));
message.success(`Done encrypting ${name}`);
} catch (err) {
setError(err);
}
};
run();
};
return {
setContent,
setError,
};
}
const addFile = useCallback(async (file: File) => { const addFile = useCallback(async (file: File) => {
if (!keys) return; if (!keys) return;
const addedFile = add(file.name); const addedFile = createFile(setFiles, `${file.name}.acs`);
const reader = new FileReader() const reader = new FileReader()
reader.onabort = addedFile.setError, reader.onabort = addedFile.setFailed,
reader.onerror = addedFile.setError, reader.onerror = addedFile.setFailed,
reader.onload = () => { reader.onload = () => {
addedFile.setContent(reader.result as string, keys); addedFile.setContent(
encrypt(keys, reader.result as string),
);
} }
reader.readAsText(file) reader.readAsText(file)
}, [keys, username]); }, [keys, username]);
const addText = useCallback(async (text: string, name: string) => { const addText = useCallback(async (text: string, name: string) => {
if (!keys) return; if (!keys) return;
const file = add(`${name}.txt`); const file = createFile(setFiles, `${name}.txt.asc`);
file.setContent(text, keys); file.setContent(
encrypt(keys, text),
);
}, [keys, username]); }, [keys, username]);
return ( return (

View File

@@ -1,3 +1,6 @@
import { nanoid } from 'nanoid';
import File from '../types/File';
export const downloadLink = (name: string, blob: Blob) => { export const downloadLink = (name: string, blob: Blob) => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
@@ -7,3 +10,49 @@ export const downloadLink = (name: string, blob: Blob) => {
downloadLink.click(); downloadLink.click();
document.body.removeChild(downloadLink); document.body.removeChild(downloadLink);
}; };
//type SetFilesType = (fn: (files: {[id: string]: File}) => {[id: string]: File}) => any;
type SetFilesType = any;
export const createFile = (setFiles: SetFilesType, name: string) => {
const id = nanoid();
const file: File = {
name,
status: 'processing',
};
setFiles((files) => ({
...files,
[id]: file,
}));
const setContent = (input: Blob | Promise<Blob>) => {
Promise.resolve(input)
.then((blob) => {
setFiles((files) => ({
...files,
[id]: {
...files[id],
blob,
status: 'success',
},
}));
})
.catch(setFailed);
}
const setFailed = (err: any) => {
setFiles((files) => ({
...files,
[id]: {
...files[id],
status: 'failed',
error: err,
},
}));
};
return {
setContent,
setFailed,
};
};

View File

@@ -8,14 +8,14 @@ type Statuses = 'packing' | 'ready';
const useDownloadAll = () => { const useDownloadAll = () => {
const [status, setStatus] = useState<Statuses>('ready'); const [status, setStatus] = useState<Statuses>('ready');
const { files } = useContext(EncryptionContext); const { files } = useContext(EncryptionContext);
const allFilesReady = Object.values(files).filter(f => f.link).length > 1; const allFilesReady = Object.values(files).filter(f => f.status === 'success').length > 1;
const downloadAll = useCallback(() => { const downloadAll = useCallback(() => {
setStatus('packing'); setStatus('packing');
const run = async () => { const run = async () => {
const zip = new Zip(); const zip = new Zip();
Object.values(files).map((file) => { Object.values(files).map((file) => {
zip.file(file.name, file.link!); zip.file(file.name, file.blob!);
}); });
const link = await zip.generateAsync({ type: 'blob' }); const link = await zip.generateAsync({ type: 'blob' });
setStatus('ready'); setStatus('ready');

44
src/screens/Decrypt.tsx Normal file
View File

@@ -0,0 +1,44 @@
import React, { useContext, useEffect, useCallback } from 'react';
import { Divider, Button } from 'antd';
import { useHistory } from 'react-router';
import FileList from '../components/FileList';
import Add from '../components/decrypt/AddFile';
import DecryptionContext from '../contexts/Decryption';
import { downloadLink } from '../helpers/files';
const Decrypt: React.FC = () => {
const history = useHistory();
const { publicKey, files, deleteFile } = useContext(DecryptionContext);
useEffect(() => {
if (localStorage.getItem('welcome') !== 'seen') {
history.replace('/welcome');
}
}, []);
const downloadPublicKey = useCallback(() => {
const publicKeyBlob = new Blob([publicKey!]);
downloadLink('public-key.asc', publicKeyBlob);
}, []);
return (
<>
<Button
onClick={downloadPublicKey}
>
Download you sharing key
</Button>
<Add />
{Object.keys(files).length > 0 && (
<>
<Divider>Files</Divider>
<FileList
files={files}
deleteFile={deleteFile}
/>
</>
)}
</>
);
};
export default Decrypt;

View File

@@ -1,13 +1,13 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import { Divider } from 'antd'; import { Divider } from 'antd';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import Add from '../components/Add'; import Add from '../components/encrypt/Add';
import FileList from '../components/FileList'; import FileList from '../components/FileList';
import EncryptionContext from '../contexts/Encryption'; import EncryptionContext from '../contexts/Encryption';
const Encrypt: React.FC = () => { const Encrypt: React.FC = () => {
const history = useHistory(); const history = useHistory();
const { files } = useContext(EncryptionContext); const { files, deleteFile } = useContext(EncryptionContext);
useEffect(() => { useEffect(() => {
if (localStorage.getItem('welcome') !== 'seen') { if (localStorage.getItem('welcome') !== 'seen') {
history.replace('/welcome'); history.replace('/welcome');
@@ -20,7 +20,14 @@ const Encrypt: React.FC = () => {
{Object.keys(files).length > 0 && ( {Object.keys(files).length > 0 && (
<> <>
<Divider>Files</Divider> <Divider>Files</Divider>
<FileList /> <FileList
files={files}
deleteFile={deleteFile}
/>
<Divider />
<i style={{ textAlign: 'center', paddingTop: '10px', display: 'block', fontSize: 12 }}>
Note: files are not send to me, you still have to download the encrypted files and send it to me.
</i>
</> </>
)} )}
</> </>

24
src/types/File.ts Normal file
View File

@@ -0,0 +1,24 @@
interface FileBase {
name: string;
status: string;
blob?: Blob;
}
interface FileProcessing extends FileBase {
name: string;
status: 'processing';
}
interface FileFailed extends FileBase {
status: 'failed',
error: any;
}
interface FileSuccess extends FileBase {
status: 'success';
blob: Blob;
}
type FileType = FileProcessing | FileFailed | FileSuccess;
export default FileType;