grub/client/Upload.tsx
2023-08-10 18:37:53 -05:00

186 lines
5 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import * as sodium from "libsodium-wrappers";
import useWebSocket from "react-use-websocket";
import CBOR from "cbor";
import { wsUrl } from "./constants";
export default function Upload({ roomId }) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [encryptionKey, setEncryptionKey] = useState(null);
const [uploadProgress, setUploadProgress] = useState(null);
const [decryptionKey, setDecryptionKey] = useState("");
const [selectedSaveFile, setSelectedSaveFile] = useState(null);
const [decryptState, setDecryptState] = useState(null);
const [pendingDecryption, setPendingDecryption] = useState([]);
let decryptionKeyInternal: Uint8Array;
if (decryptionKey) {
decryptionKeyInternal = hex2Bytes(decryptionKey);
}
const [writeStream, setWriteStream] = useState(null);
const readyToDownload =
!!decryptionKeyInternal && !!selectedSaveFile && !!writeStream;
const {
sendMessage,
lastJsonMessage,
readyState: newReadyState,
} = useWebSocket(wsUrl, {
share: true,
onMessage: async (event) => {
const message = CBOR.decode(await event.data.arrayBuffer());
if (message.type === "FileUploadBegin") {
const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
message.header,
decryptionKeyInternal
);
setDecryptState(state);
}
if (message.type === "FileUploadChunk") {
if (readyToDownload && decryptState) {
for (const { data } of [...pendingDecryption, message]) {
const { message } =
sodium.crypto_secretstream_xchacha20poly1305_pull(
decryptState,
data
);
const result = await writeStream.write(message);
}
setPendingDecryption([]);
} else {
setPendingDecryption([...pendingDecryption, message]);
}
}
if (message.type === "FileUploadComplete") {
await writeStream.close();
}
},
});
function sendCborMessage(data) {
let cbor = CBOR.encode(data);
sendMessage(cbor);
}
useEffect(() => {
(async () => {
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
setEncryptionKey(key);
})();
}, []);
const uploadFile = useCallback(async () => {
if (!encryptionKey) return;
if (!selectedFile) return;
const { state, header } =
sodium.crypto_secretstream_xchacha20poly1305_init_push(encryptionKey);
sendCborMessage({
type: "FileUploadBegin",
room_id: roomId,
header,
});
const stream = selectedFile.stream();
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Encrypt this chunk
const encryptedValue =
sodium.crypto_secretstream_xchacha20poly1305_push(
state,
value,
null,
0
);
sendCborMessage({
type: "FileUploadChunk",
room_id: roomId,
data: encryptedValue,
});
}
sendCborMessage({
type: "FileUploadComplete",
room_id: roomId,
});
} finally {
reader.releaseLock();
}
}, [encryptionKey, selectedFile]);
async function selectSaveFile() {
const newHandle = await window.showSaveFilePicker();
setSelectedSaveFile(newHandle);
const writableStream = await newHandle.createWritable();
setWriteStream(writableStream);
}
return (
<div>
<h3>Send file</h3>
<p>Upload Key: {encryptionKey && bytes2Hex(encryptionKey)}</p>
<input type="file" onChange={(e) => setSelectedFile(e.target.files[0])} />
<button onClick={uploadFile}>upload</button>
<hr />
<h3>Receive file</h3>
<p>
<ol>
<li>
<input
type="text"
value={decryptionKey}
onChange={(e) => setDecryptionKey(e.target.value)}
placeholder="Download Key..."
/>
</li>
<li>
<button onClick={selectSaveFile} disabled={!!selectedSaveFile}>
{selectedSaveFile
? `Saving to "${selectedSaveFile.name}"`
: "select save file"}
</button>
</li>
</ol>
(
{readyToDownload
? "you are ready to download"
: "you are not ready to download, make sure you enter the key and select a place to save"}
)
</p>
</div>
);
}
function bytes2Hex(byteArray: Uint8Array): string {
return Array.prototype.map
.call(byteArray, function (byte: number) {
return ("0" + (byte & 0xff).toString(16)).slice(-2);
})
.join("");
}
function hex2Bytes(hexString: string): Uint8Array {
let result = [];
for (var i = 0; i < hexString.length; i += 2) {
result.push(parseInt(hexString.substr(i, 2), 16));
}
return new Uint8Array(result);
}