diff --git a/Cargo.lock b/Cargo.lock index 1ea0902..c5dfadf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,6 +512,7 @@ dependencies = [ "futures", "mzlib", "serde", + "serde_bytes", "serde_json", "tokio", "uuid", @@ -972,6 +973,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.183" diff --git a/Cargo.toml b/Cargo.toml index b68821a..58c75ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ derivative = "2.2.0" flume = "0.10.14" futures = "0.3.28" serde = { version = "1.0.183", features = ["derive"] } +serde_bytes = "0.11.12" serde_json = "1.0.104" tokio = { version = "1.30.0", features = ["full"] } uuid = "1.4.1" diff --git a/client/App.tsx b/client/App.tsx index 7abbac9..b80c1b0 100644 --- a/client/App.tsx +++ b/client/App.tsx @@ -9,9 +9,5 @@ export default function App() { location.href = `/${v4()}`; } - return ( - <> - Hellosu, - - ); + return ; } diff --git a/client/Room.tsx b/client/Room.tsx index 9fe754f..50eb668 100644 --- a/client/Room.tsx +++ b/client/Room.tsx @@ -3,6 +3,8 @@ import { useEffect, useState } from "react"; import useWebSocket, { ReadyState } from "react-use-websocket"; import * as Automerge from "@automerge/automerge"; import * as uuid from "uuid"; +import {} from "libsodium"; +import Upload from "./Upload"; const connectionStatusMap = { [ReadyState.CONNECTING]: "Connecting", @@ -33,6 +35,7 @@ export default function Room({ roomId }) { lastJsonMessage, readyState: newReadyState, } = useWebSocket("ws://localhost:3100/ws", { + share: true, onOpen: ({}) => { console.log("Shiet, connected."); }, @@ -130,6 +133,8 @@ export default function Room({ roomId }) { return ( <> + +

Room Id: {roomId}

Connection status: {connectionStatus}

Connected: diff --git a/client/Upload.tsx b/client/Upload.tsx new file mode 100644 index 0000000..a892bfa --- /dev/null +++ b/client/Upload.tsx @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useState } from "react"; +import * as sodium from "libsodium-wrappers"; +import useWebSocket from "react-use-websocket"; +import CBOR from "cbor"; + +export default function Upload({ roomId }) { + const [selectedFile, setSelectedFile] = useState(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("ws://localhost:3100/ws", { + 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 + ); + console.log("header", message.header); + console.log("state", state); + setDecryptState(state); + } + + if (message.type === "FileUploadChunk") { + console.log("data", message); + + if (readyToDownload && decryptState) { + for (const { data } of [...pendingDecryption, message]) { + const { message } = + sodium.crypto_secretstream_xchacha20poly1305_pull( + decryptState, + data + ); + const result = await writeStream.write(message); + console.log("output", result, message); + } + setPendingDecryption([]); + } else { + setPendingDecryption([...pendingDecryption, message]); + } + } + + if (message.type === "FileUploadComplete") { + await writeStream.close(); + console.log("done!"); + } + }, + }); + + 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; + + console.log("file", selectedFile); + 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, + }); + console.log("sent encrypted chunk", encryptedValue); + } + + sendCborMessage({ + type: "FileUploadComplete", + room_id: roomId, + }); + console.log("sent done"); + } finally { + reader.releaseLock(); + } + }, [encryptionKey, selectedFile]); + + async function selectSaveFile() { + const newHandle = await window.showSaveFilePicker(); + console.log("handle", newHandle); + setSelectedSaveFile(newHandle); + + const writableStream = await newHandle.createWritable(); + setWriteStream(writableStream); + } + + return ( +
+

Send file

+ +

Upload Key: {encryptionKey && bytes2Hex(encryptionKey)}

+ + setSelectedFile(e.target.files[0])} /> + + +
+ +

Receive file

+ +

+

    +
  1. + setDecryptionKey(e.target.value)} + placeholder="Download Key..." + /> +
  2. +
  3. + +
  4. +
+ ( + {readyToDownload + ? "you are ready to download" + : "you are not ready to download, make sure you enter the key and select a place to save"} + ) +

+
+ ); +} + +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); +} diff --git a/package-lock.json b/package-lock.json index 819b874..06a831b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,8 @@ "dependencies": { "@automerge/automerge": "^2.0.3", "cbor": "npm:@jprochazk/cbor@^0.5.0", + "libsodium": "^0.7.11", + "libsodium-wrappers": "^0.7.11", "localforage": "^1.10.0", "match-sorter": "^6.3.1", "react": "^18.2.0", @@ -17,6 +19,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@types/libsodium-wrappers": "^0.7.10", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/uuid": "^9.0.2", @@ -1011,6 +1014,12 @@ "node": ">=10" } }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -1331,6 +1340,19 @@ "node": ">=6" } }, + "node_modules/libsodium": { + "version": "0.7.11", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.11.tgz", + "integrity": "sha512-WPfJ7sS53I2s4iM58QxY3Inb83/6mjlYgcmZs7DJsvDlnmVUwNinBCi5vBT43P6bHRy01O4zsMU2CoVR6xJ40A==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.11", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.11.tgz", + "integrity": "sha512-SrcLtXj7BM19vUKtQuyQKiQCRJPgbpauzl3s0rSwD+60wtHqSUuqcoawlMDheCJga85nKOQwxNYQxf/CKAvs6Q==", + "dependencies": { + "libsodium": "^0.7.11" + } + }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", diff --git a/package.json b/package.json index b987233..c8b3de4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "dev": "vite" }, "devDependencies": { + "@types/libsodium-wrappers": "^0.7.10", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/uuid": "^9.0.2", @@ -15,6 +16,8 @@ "dependencies": { "@automerge/automerge": "^2.0.3", "cbor": "npm:@jprochazk/cbor@^0.5.0", + "libsodium": "^0.7.11", + "libsodium-wrappers": "^0.7.11", "localforage": "^1.10.0", "match-sorter": "^6.3.1", "react": "^18.2.0", diff --git a/src/main.rs b/src/main.rs index 6c16317..14d2b3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ struct Room { document: Automerge, sync_state: SyncState, connected_clients: DashSet, + current_uploader: Option, } impl Room { @@ -193,7 +194,6 @@ async fn handle_socket(state: AppState, socket: WebSocket) -> Result<()> { .0 .entry(room_id.clone()) .or_insert_with(|| Room::default()); - rooms.insert(room_id.clone()); // Add to the list of connected clients let connected_clients = room.connected_clients.clone(); @@ -203,6 +203,37 @@ async fn handle_socket(state: AppState, socket: WebSocket) -> Result<()> { // Broadcast to the room broadcast(&state, connected_clients.clone(), message).await; } + + Message::FileUploadBegin { room_id, .. } + | Message::FileUploadComplete { room_id } + | Message::FileUploadChunk { room_id, .. } => { + let mut room = state + .rooms + .0 + .entry(room_id.clone()) + .or_insert_with(|| Room::default()); + + let connected_clients = room.connected_clients.clone(); + connected_clients.remove(&client_id); + + match &message { + Message::FileUploadBegin { header, .. } => { + match room.current_uploader { + Some(v) if v == client_id => {} + Some(v) => continue, + None => room.current_uploader = Some(client_id.clone()), + } + } + Message::FileUploadComplete { .. } => { + room.current_uploader = None; + } + Message::FileUploadChunk { data, .. } => {} + _ => {} + }; + + broadcast(&state, connected_clients, message).await; + } + _ => {} }; } diff --git a/src/message.rs b/src/message.rs index e5870f8..f0586ed 100644 --- a/src/message.rs +++ b/src/message.rs @@ -38,6 +38,19 @@ pub enum Message { #[derivative(Debug = "ignore")] message: AutomergeMessage, }, + FileUploadBegin { + room_id: Uuid, + #[serde(with = "serde_bytes")] + header: Vec, + }, + FileUploadChunk { + room_id: Uuid, + #[serde(with = "serde_bytes")] + data: Vec, + }, + FileUploadComplete { + room_id: Uuid, + }, } impl Message { diff --git a/tsconfig.json b/tsconfig.json index 76e25f8..87436eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,9 @@ { - "compilerOptions": { "lib": ["ESNext", "DOM"], "jsx": "react-jsx" } + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "NodeNext", + "jsx": "react-jsx" + } }