185 lines
5 KiB
TypeScript
185 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);
|
|
}
|