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])} />
+
upload
+
+
+
+
Receive file
+
+
+
+
+ setDecryptionKey(e.target.value)}
+ placeholder="Download Key..."
+ />
+
+
+
+ {selectedSaveFile
+ ? `Saving to "${selectedSaveFile.name}"`
+ : "select save file"}
+
+
+
+ (
+ {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"
+ }
}