Entendu/assets/js/pages/JustPage.tsx

218 lines
5.6 KiB
TypeScript

import React, { useEffect, useState } from "react";
import {
ProgressIndicator,
Header2,
Button,
IconArrow,
Label,
FileInput,
TextArea,
CenteredContainer,
Spacer,
TextAlignWrapper,
GlobalStyle,
} from "@intended/intended-ui";
import HexMix from "../utils/hexmix";
type JustPageProps = {
csrf: string;
};
interface AESKey {
key: CryptoKey;
iv: Uint8Array;
exported: ArrayBuffer;
keyHex: string;
ivHex: string;
}
const JustPage = (props: JustPageProps) => {
const [secretInput, setSecretInput] = useState("");
const [fileInput, setFileInput] = useState<string | null>(null);
const [fileName, setFileName] = useState("");
const [fileType, setFileType] = useState("");
useEffect(() => {
sessionStorage.clear();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setSecretInput(e.target.value);
};
const handleFile = (file: File) => {
setFileName(file.name);
setFileType(file.type);
if (file instanceof File) {
if (file.size > 2097152) {
// TODO: may not need this check
alert("Error: Max file size is 2mb.");
return;
}
const reader = new FileReader();
reader.onloadend = loadFile;
reader.readAsArrayBuffer(file);
}
};
const loadFile = (fileEvent: ProgressEvent<FileReader>) => {
// Make sure we actually got a binary file
if (
fileEvent &&
fileEvent.target &&
fileEvent.target.result instanceof ArrayBuffer
) {
const data = fileEvent.target.result as ArrayBuffer;
HexMix.arrayBufferToString(data, (result: string) => {
setFileInput(result);
});
} else {
alert("File is either missing or corrupt.");
}
};
const fileFormData = async (
form: FormData,
aesKey: AESKey
): Promise<FormData> => {
if (!fileInput) return form;
const encoded = HexMix.stringToArrayBuffer(fileInput as string);
const encrypted = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: aesKey.iv,
},
aesKey.key,
encoded
);
const blobData = new Blob([encrypted]);
form.append("file_content", blobData, fileName);
form.append("filename", fileName);
form.append("filetype", fileType);
HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_file", result);
});
return form;
};
const textFormData = async (
form: FormData,
aesKey: AESKey
): Promise<FormData> => {
if (!secretInput) return form;
const encoded = HexMix.stringToArrayBuffer(secretInput);
const encrypted = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: aesKey.iv,
},
aesKey.key,
encoded
);
const blobData = new Blob([encrypted]);
form.append("text_content", blobData, "secret_message.txt");
HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_message", result);
});
return form;
};
const createKey = async (): Promise<AESKey> => {
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const exported = await window.crypto.subtle.exportKey("raw", key);
const keyHex = HexMix.uint8ToHex(new Uint8Array(exported));
const ivHex = HexMix.uint8ToHex(iv);
return {
key,
iv,
exported,
keyHex,
ivHex,
};
};
const postContents = async () => {
if (!window.crypto.subtle) {
alert("Browser does not support SubtleCrypto");
return;
}
const key = await createKey();
let formData = new FormData();
formData = await fileFormData(formData, key);
formData = await textFormData(formData, key);
try {
const link: Response = await fetch(`${window.location.origin}/just`, {
headers: {
"X-CSRF-Token": props.csrf,
},
body: formData,
method: "POST",
});
const { id: link_id } = await link.json();
sessionStorage.setItem("link_id", link_id);
sessionStorage.setItem("key_hex", key.keyHex);
sessionStorage.setItem("iv_hex", key.ivHex);
window.location.href = `${window.location.origin}/just/for`;
} catch (err: any) {
alert(err.message);
}
};
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen className="centered-container">
<CenteredContainer>
<ProgressIndicator currentProgress={1} />
<Header2>Create a secret</Header2>
<TextAlignWrapper align="left">
<Label htmlFor="secretInput">Enter your secret here</Label>
</TextAlignWrapper>
<TextArea
id="secretInput"
value={secretInput}
onChange={handleChange}
placeholder="Tell me your secrets"
/>
<Spacer space="2rem" />
<TextAlignWrapper align="center">
<Label htmlFor="fileInput">OR</Label>
</TextAlignWrapper>
<Spacer space="1.6rem" />
<FileInput id="fileInput" value={fileName} handleFile={handleFile} />
<Spacer space="4rem" />
<div
style={{
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<Button variant="secondary" onClick={postContents}>
<IconArrow arrowDirection="right" />
</Button>
</div>
</CenteredContainer>
</CenteredContainer>
</React.StrictMode>
);
};
export default JustPage;