import { useCallback, useContext, useEffect, useState } from "react";
import * as sodium from "libsodium-wrappers-sumo";
import useWebSocket from "react-use-websocket";
import CBOR from "cbor";
import { wsUrl } from "./constants";
import { RoomContext } from "./lib/roomContext";
import { useDebouncedCallback } from "use-debounce";
import {
Row,
Col,
Button,
Form,
Badge,
OverlayTrigger,
Tooltip,
} from "react-bootstrap";
export default function Upload() {
const { roomId } = useContext(RoomContext);
const [selectedFile, setSelectedFile] = useState(null);
const [encryptionKey, setEncryptionKey] = useState(null);
const [passphrase, setPassphrase] = useState("");
const [uploadProgress, setUploadProgress] = useState(null);
const setHashedPassphrase = useDebouncedCallback(
(passphrase) => {
if (passphrase) {
const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
console.log("salt", salt);
setEncryptionKey(
sodium.crypto_pwhash(
sodium.crypto_box_SEEDBYTES,
passphrase,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_ARGON2ID13
)
);
}
},
// delay in ms
1000
);
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 } = 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;
if (passphrase) {
setHashedPassphrase(passphrase);
} else {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
setEncryptionKey(key);
}
})();
}, [passphrase]);
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 (
<>
How does this work?
RECEIVER MUST BE USING CHROME{" "}
[?]
-
Uploader: Copy the key and send it to whoever you're sending
files to
-
Receiver: Paste the key into the "Paste key here" box
-
Receiver: Press the "select save file" button to choose where
to save the file to
-
Receiver: Tell the uploader to press "upload"
-
Uploader: Press "upload"
Only one person can upload at a time.
Send file
{/*
Upload passphrase:
{
setEncryptionKey(null);
setPassphrase(e.target.value);
}}
/>
*/}
Upload Key:{" "}
setSelectedFile(e.target.files[0])}
/>
Receive file
{readyToDownload ? (
ready
) : (
you are not ready to download, make sure you enter the key
and select a place to save
}
>
not ready
)}
-
setDecryptionKey(e.target.value)}
placeholder="Paste key here..."
/>
-
>
);
}
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);
}