this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: authorized syncing

authored by

Anish Lakhwara and committed by
sōm
d785dde3 bd957d9e

+905 -18
+6
db/migrations/auth.sql
··· 1 + CREATE TABLE IF NOT EXISTS rooms ( 2 + room_id TEXT PRIMARY KEY, 3 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 4 + ); 5 + 6 + CREATE INDEX IF NOT EXISTS idx_rooms_room_id ON rooms(room_id);
+34 -1
mast-react-vite/src/App.tsx
··· 16 16 project: string; 17 17 }; 18 18 19 - function App({ ctx }) { 19 + function App({ ctx, syncWorker, dbname, roomId }) { 20 20 const [newText, setNewText] = useState(""); 21 21 const { selectedItems, clearSelection, getSelectionString } = useSelection(); 22 22 const [currentAction, setCurrentAction] = useState("add"); ··· 304 304 break; 305 305 } 306 306 setNewText(""); 307 + 308 + // Explicitly tell the worker to sync changes after modifying the database 309 + console.log("Database changed - triggering sync"); 310 + setTimeout(() => { 311 + syncWorker.postMessage({ 312 + type: 'SYNC_CHANGES', 313 + dbname 314 + }); 315 + }, 100); // Small delay to ensure changes are committed 307 316 } catch (error) { 308 317 // TODO: 309 318 // This is actually bad ··· 359 368 <section className="flex-1 container py-12 h-[calc(100vh-theme(spacing.4))] overflow-hidden relative"> 360 369 <SidebarTrigger className="absolute top-4 left-8 border border-foreground" /> 361 370 <div className="absolute top-4 right-12 flex gap-2"> 371 + <button 372 + onClick={() => { 373 + console.log("Manual sync requested"); 374 + syncWorker.postMessage({ 375 + type: 'SYNC_CHANGES', 376 + dbname 377 + }); 378 + }} 379 + className="px-3 py-1 bg-blue-600 text-white rounded-md text-xs hover:bg-blue-700" 380 + > 381 + Sync Now 382 + </button> 362 383 {selectedItems.size > 0 && ( 363 384 <Badge variant="default"> {selectedItems.size} selected </Badge> 364 385 )} ··· 402 423 <div className="flex-1 bg-muted border-b border-foreground h-14 absolute inset-x-0 top-0 " /> 403 424 <SidebarTrigger className="fixed top-4 border border-foreground left-8" /> 404 425 <div className="absolute top-4 right-12 flex gap-2"> 426 + <button 427 + onClick={() => { 428 + console.log("Manual sync requested (mobile)"); 429 + syncWorker.postMessage({ 430 + type: 'SYNC_CHANGES', 431 + dbname 432 + }); 433 + }} 434 + className="px-3 py-1 bg-blue-600 text-white rounded-md text-xs hover:bg-blue-700" 435 + > 436 + Sync 437 + </button> 405 438 {selectedItems.size > 0 && ( 406 439 <Badge variant="default"> {selectedItems.size} selected </Badge> 407 440 )}
+314 -9
mast-react-vite/src/main.tsx
··· 14 14 import SyncWorker from './worker/sync-worker.ts?worker'; 15 15 const syncWorker = new SyncWorker(); 16 16 17 + // Store active crypto key pairs (in memory for the session) 18 + const activeKeyPairs: Record<string, CryptoKeyPair> = {}; 19 + 20 + // Function to generate a WebCrypto key pair 21 + async function generateKeyPair() { 22 + try { 23 + const keyPair = await window.crypto.subtle.generateKey( 24 + { 25 + name: "ECDSA", 26 + namedCurve: "P-256", // Using P-256 curve for good security/performance balance 27 + }, 28 + true, // extractable 29 + ["sign", "verify"] // operations allowed with this key 30 + ); 31 + 32 + // Export the public key to store/transmit it 33 + const publicKeyBuffer = await window.crypto.subtle.exportKey( 34 + "spki", // Standard format for public keys 35 + keyPair.publicKey 36 + ); 37 + 38 + // Convert to base64 for storage/transmission 39 + const publicKeyBase64 = btoa( 40 + String.fromCharCode.apply(null, new Uint8Array(publicKeyBuffer)) 41 + ); 42 + 43 + console.log("KeyPair: " + publicKeyBase64) 44 + 45 + return { 46 + keyPair, 47 + publicKeyBase64 48 + }; 49 + } catch (error) { 50 + console.error("Error generating key pair:", error); 51 + throw error; 52 + } 53 + } 54 + 17 55 // Function to get room ID from URL hash or generate a new one 18 - function getRoomId(): string { 56 + async function getRoomId(): Promise<{ roomId: string, keyPair?: CryptoKeyPair, publicKeyBase64?: string }> { 19 57 // Check URL hash for room parameter 20 58 const hash = window.location.hash.substring(1); 21 59 const params = new URLSearchParams(hash); 22 60 const roomFromHash = params.get('room'); 23 61 24 62 if (roomFromHash) { 25 - return roomFromHash; 63 + // Room exists in URL, check if we have keys for it in localStorage 64 + const storedKeys = localStorage.getItem(`keys-${roomFromHash}`); 65 + if (storedKeys) { 66 + // We have keys for this room, parse and log them 67 + const keyData = JSON.parse(storedKeys); 68 + console.log(`Loading existing keys for room: ${roomFromHash}`); 69 + console.log(`Using existing public key: ${keyData.publicKey.substring(0, 20)}... (created: ${keyData.created})`); 70 + 71 + // Load the key into memory for signing - for now, we just return the public key 72 + // In a full implementation, we would securely retrieve the private key as well 73 + try { 74 + console.log("Found existing key for room"); 75 + // Note that we're not returning a keyPair here since we don't have the private key 76 + // The calling code will handle this case by just registering the public key 77 + return { 78 + roomId: roomFromHash, 79 + publicKeyBase64: keyData.publicKey 80 + }; 81 + } catch (error) { 82 + console.error("Error processing existing key:", error); 83 + return { roomId: roomFromHash }; 84 + } 85 + } 86 + 87 + // We have a room from URL but no keys - generate keys for this existing room 88 + console.log("Room found in URL but no keys - generating new keys"); 89 + const { keyPair, publicKeyBase64 } = await generateKeyPair(); 90 + 91 + // Store public key in localStorage 92 + localStorage.setItem(`keys-${roomFromHash}`, JSON.stringify({ 93 + publicKey: publicKeyBase64, 94 + created: new Date().toISOString() 95 + })); 96 + 97 + return { 98 + roomId: roomFromHash, 99 + keyPair, 100 + publicKeyBase64 101 + }; 26 102 } 27 103 28 - // Check localStorage 104 + // Check localStorage for room 29 105 const storedRoom = localStorage.getItem("room"); 30 106 if (storedRoom) { 31 - return storedRoom; 107 + // Room exists in localStorage, check if we have keys for it 108 + const storedKeys = localStorage.getItem(`keys-${storedRoom}`); 109 + if (storedKeys) { 110 + // We have keys for this room, parse and log them 111 + const keyData = JSON.parse(storedKeys); 112 + console.log(`Loading existing keys for room from localStorage: ${storedRoom}`); 113 + console.log(`Using existing public key: ${keyData.publicKey.substring(0, 20)}... (created: ${keyData.created})`); 114 + 115 + // Load the key into memory for signing - for now, we just return the public key 116 + try { 117 + console.log("Found existing key in localStorage"); 118 + // Note that we're not returning a keyPair here since we don't have the private key 119 + // The calling code will handle this case by just registering the public key 120 + return { 121 + roomId: storedRoom, 122 + publicKeyBase64: keyData.publicKey 123 + }; 124 + } catch (error) { 125 + console.error("Error processing existing key from localStorage:", error); 126 + return { roomId: storedRoom }; 127 + } 128 + } 129 + 130 + // We have a room in localStorage but no keys - generate keys 131 + console.log("Room found in localStorage but no keys - generating new keys"); 132 + const { keyPair, publicKeyBase64 } = await generateKeyPair(); 133 + 134 + // Store public key in localStorage 135 + localStorage.setItem(`keys-${storedRoom}`, JSON.stringify({ 136 + publicKey: publicKeyBase64, 137 + created: new Date().toISOString() 138 + })); 139 + 140 + return { 141 + roomId: storedRoom, 142 + keyPair, 143 + publicKeyBase64 144 + }; 32 145 } 33 146 34 - // Generate a new room ID if none exists 147 + // Generate a new room ID and key pair 35 148 const newRoomId = crypto.randomUUID().replaceAll("-", ""); 36 149 localStorage.setItem("room", newRoomId); 150 + 151 + // Generate a new key pair for this room 152 + const { keyPair, publicKeyBase64 } = await generateKeyPair(); 153 + 154 + // Store public key in localStorage 155 + localStorage.setItem(`keys-${newRoomId}`, JSON.stringify({ 156 + publicKey: publicKeyBase64, 157 + created: new Date().toISOString() 158 + })); 37 159 38 160 // Update URL with the new room ID 39 161 updateUrlWithRoom(newRoomId); 162 + 163 + console.log("KeyPair: " + publicKeyBase64) 40 164 41 - return newRoomId; 165 + return { 166 + roomId: newRoomId, 167 + keyPair, 168 + publicKeyBase64 169 + }; 42 170 } 43 171 44 - // Function to update URL with room ID 172 + 45 173 function updateUrlWithRoom(roomId: string) { 46 174 const hash = window.location.hash.substring(1); 47 175 const params = new URLSearchParams(hash); ··· 66 194 const initDb = async () => { 67 195 const sqlite = await initWasm(() => wasmUrl); 68 196 69 - // Get or create room ID 70 - const roomId = getRoomId(); 197 + // Get or create room ID and keys 198 + const { roomId, keyPair, publicKeyBase64 } = await getRoomId(); 199 + 200 + // In this case, we've already got the publicKeyBase64 from getRoomId 201 + // so we can skip the extraction of existing keys 202 + 203 + // If we have a key pair from generation, store it in memory 204 + if (keyPair) { 205 + console.log("Generated new key pair for room:", roomId); 206 + console.log("Public key:", publicKeyBase64); 207 + 208 + // Store key pair in memory for the session 209 + activeKeyPairs[roomId] = keyPair; 210 + } else if (publicKeyBase64) { 211 + console.log("Using existing public key for room:", roomId); 212 + console.log("Public key:", publicKeyBase64.substring(0, 20) + "..."); 213 + 214 + // Generate a new key pair for this session 215 + console.log("Generating new key pair for existing public key"); 216 + try { 217 + const { keyPair: newKeyPair } = await generateKeyPair(); 218 + console.log("Generated new key pair for signing during this session"); 219 + 220 + // Store the new key pair in memory for this session 221 + activeKeyPairs[roomId] = newKeyPair; 222 + } catch (error) { 223 + console.error("Error generating new key pair for signing:", error); 224 + } 225 + } 226 + 227 + // Register the key with the server (either new or existing) 228 + if (publicKeyBase64) { 229 + await registerKeyWithServer(roomId, publicKeyBase64); 230 + } 231 + 232 + // Force an initial sync after registration 233 + console.log("Triggering initial sync..."); 71 234 72 235 // Use room ID in database name to isolate data per room 73 236 const dbname = `todo-${roomId}.db`; ··· 102 265 const rx = tblrx(db); 103 266 return { db, rx, roomId, dbname }; 104 267 }; 268 + 269 + // Function to register a key with the server 270 + async function registerKeyWithServer(roomId: string, publicKey: string): Promise<boolean> { 271 + try { 272 + console.log("Registering public key with server for room:", roomId); 273 + console.log("Public key to register:", publicKey.substring(0, 20) + "..."); 274 + 275 + const requestBody = { 276 + roomId: roomId, 277 + publicKey: publicKey, 278 + permissions: { 279 + read: true, 280 + write: true, 281 + invite: true 282 + } 283 + }; 284 + 285 + console.log("Request body:", JSON.stringify(requestBody)); 286 + 287 + const response = await fetch('http://localhost:8080/auth/register-key', { 288 + method: 'POST', 289 + headers: { 290 + 'Content-Type': 'application/json', 291 + 'Accept': 'application/json', 292 + }, 293 + mode: 'cors', 294 + credentials: 'omit', 295 + body: JSON.stringify(requestBody), 296 + }); 297 + 298 + const result = await response.json(); 299 + console.log("Key registration response:", result); 300 + 301 + if (!response.ok) { 302 + console.error("Failed to register public key with server:", result.message); 303 + return false; 304 + } else { 305 + console.log("Successfully registered public key with server"); 306 + return true; 307 + } 308 + } catch (error) { 309 + console.error("Error registering public key:", error); 310 + return false; 311 + } 312 + } 313 + 314 + // Function to sign a payload using a room's key pair 315 + async function signPayload(roomId: string, payload: any): Promise<{signature: string, publicKey: string} | null> { 316 + console.log(`Signing payload for room ${roomId}`); 317 + 318 + // Check if we have the key pair in memory 319 + const keyPair = activeKeyPairs[roomId]; 320 + if (!keyPair) { 321 + console.error(`No key pair found in memory for room ${roomId}`); 322 + console.log("Attempting to regenerate key pair from storage..."); 323 + 324 + // For real implementation, we would need to securely store/retrieve the private key 325 + // This is just a placeholder for demonstration 326 + console.error("Cannot sign without private key - please reload the page to generate a new keypair"); 327 + return null; 328 + } 329 + 330 + try { 331 + // Get the public key 332 + const storedKeys = localStorage.getItem(`keys-${roomId}`); 333 + if (!storedKeys) { 334 + console.error(`No stored keys found for room ${roomId}`); 335 + return null; 336 + } 337 + 338 + const keyData = JSON.parse(storedKeys); 339 + const publicKeyBase64 = keyData.publicKey; 340 + console.log(`Using public key: ${publicKeyBase64.substring(0, 20)}...`); 341 + 342 + // Stringify and encode the payload 343 + const payloadString = JSON.stringify(payload); 344 + const payloadBytes = new TextEncoder().encode(payloadString); 345 + console.log(`Payload to sign (${payloadBytes.length} bytes): ${payloadString.substring(0, 50)}...`); 346 + 347 + // Sign the payload 348 + console.log('Signing payload with ECDSA/SHA-256...'); 349 + const signature = await window.crypto.subtle.sign( 350 + { 351 + name: "ECDSA", 352 + hash: {name: "SHA-256"}, 353 + }, 354 + keyPair.privateKey, 355 + payloadBytes 356 + ); 357 + 358 + // Convert signature to base64 359 + const signatureArray = new Uint8Array(signature); 360 + const signatureBase64 = btoa(String.fromCharCode.apply(null, signatureArray)); 361 + console.log(`Generated signature (${signatureArray.length} bytes): ${signatureBase64.substring(0, 20)}...`); 362 + 363 + return { 364 + signature: signatureBase64, 365 + publicKey: publicKeyBase64 366 + }; 367 + } catch (error) { 368 + console.error("Error signing payload:", error); 369 + return null; 370 + } 371 + } 372 + 373 + // Handle worker messages 374 + syncWorker.addEventListener('message', async (event) => { 375 + const { type, dbname, payload, room } = event.data; 376 + 377 + if (type === 'REQUEST_SIGNATURE') { 378 + console.log('Received request for signature', { dbname, room }); 379 + 380 + // Sign the payload 381 + const signatureData = await signPayload(room, payload); 382 + 383 + if (!signatureData) { 384 + console.error(`Failed to sign payload for room ${room}`); 385 + return; 386 + } 387 + 388 + // Create the authenticated message 389 + const authMessage = { 390 + ...payload, 391 + publicKey: signatureData.publicKey, 392 + signature: signatureData.signature 393 + }; 394 + 395 + // We don't need to create a new WebSocket connection here 396 + // The worker already has an open connection 397 + 398 + // Find the WebSocket instance - this is a simplified approach 399 + // In a real implementation, we would have a more robust way to get the WebSocket 400 + setTimeout(() => { 401 + // Send the message to the worker which will forward it to the server 402 + syncWorker.postMessage({ 403 + type: 'SEND_SIGNED_MESSAGE', 404 + dbname, 405 + message: authMessage 406 + }); 407 + }, 0); 408 + } 409 + }); 105 410 106 411 const init = async () => { 107 412 const ctx = await initDb();
+109 -8
mast-react-vite/src/worker/sync-worker.ts
··· 26 26 27 27 // Handle messages from the main thread 28 28 self.onmessage = async (event) => { 29 - const { type, dbname, config } = event.data; 29 + const { type, dbname, config, message } = event.data; 30 30 logDebug(`Received message: ${type}`, { dbname, config }); 31 31 32 32 switch (type) { ··· 38 38 break; 39 39 case 'SYNC_CHANGES': 40 40 // This is triggered when React tells us changes happened 41 - await sendChanges(dbname); 41 + logDebug(`Received explicit sync request for ${dbname}`); 42 + try { 43 + await sendChanges(dbname); 44 + logDebug(`Manual sync completed for ${dbname}`); 45 + } catch (error) { 46 + logError(`Error during manual sync: ${error}`); 47 + } 48 + break; 49 + case 'SEND_SIGNED_MESSAGE': 50 + // Send a pre-signed message to the server 51 + await sendSignedMessage(dbname, message); 42 52 break; 43 53 } 44 54 }; 45 55 56 + // Function to send a pre-signed message to the server 57 + async function sendSignedMessage(dbname: string, message: any) { 58 + const connection = connections[dbname]; 59 + if (!connection) { 60 + logDebug(`Cannot send signed message - no connection for ${dbname}`); 61 + return; 62 + } 63 + 64 + if (!connection.ws || connection.ws.readyState !== WebSocket.OPEN) { 65 + logDebug(`Cannot send signed message - WebSocket not open for ${dbname}`); 66 + return; 67 + } 68 + 69 + try { 70 + logDebug(`Sending signed message for ${dbname}`); 71 + logDebug(`Message type: ${message.type}, with data count: ${message.data?.length || 0}`); 72 + if (message.publicKey) { 73 + logDebug(`Authenticated with public key: ${message.publicKey.substring(0, 20)}... and signature length: ${message.signature?.length || 0}`); 74 + } 75 + connection.ws.send(JSON.stringify(message)); 76 + 77 + // If this was a changes message, update the last sync version 78 + if (message.type === "changes" && Array.isArray(message.data) && message.data.length > 0) { 79 + logDebug(`Sending ${message.data.length} changes to server via WebSocket`); 80 + 81 + // If the message is authenticated, log that 82 + if (message.publicKey && message.signature) { 83 + logDebug(`Message is authenticated with public key: ${message.publicKey.substring(0, 20)}... and signature`); 84 + } else { 85 + logDebug(`Message is NOT authenticated - no signature attached`); 86 + } 87 + 88 + const versions = message.data.map((change: any) => Number(change.DBVersion)); 89 + const maxVersion = Math.max(...versions); 90 + if (maxVersion > connection.lastSyncVersion) { 91 + connection.lastSyncVersion = maxVersion; 92 + logDebug(`Updated lastSyncVersion to ${maxVersion}`); 93 + } 94 + 95 + // Notify main thread that changes were sent 96 + self.postMessage({ type: 'CHANGES_SENT', dbname, count: message.data.length }); 97 + } 98 + } catch (error) { 99 + logError(`Error sending signed message:`, error); 100 + self.postMessage({ type: 'SYNC_ERROR', dbname, error: 'Failed to send signed message' }); 101 + } 102 + }; 103 + 46 104 // Start syncing a database 47 105 async function startSync(dbname: string, config: { room: string, url: string }) { 48 106 try { ··· 108 166 109 167 // Set up WebSocket event handlers 110 168 ws.onopen = () => { 111 - logDebug(`WebSocket connected for ${dbname}`); 169 + logDebug(`WebSocket connected for ${dbname} (state: ${ws.readyState})`); 112 170 connections[dbname].isConnecting = false; 113 171 114 172 // Set up change listener using onUpdate 115 173 db.onUpdate(async () => { 116 174 logDebug(`Database update detected for ${dbname}`); 117 - await sendChanges(dbname); 175 + logDebug(`Sending changes to server for ${dbname}...`); 176 + 177 + // Add a small delay to ensure the changes are committed 178 + setTimeout(async () => { 179 + await sendChanges(dbname); 180 + }, 100); 118 181 }); 119 182 120 183 // Initial sync - request changes from server ··· 131 194 ws.onmessage = async (event) => { 132 195 try { 133 196 logDebug(`Received WebSocket message: ${event.data.substring(0, 100)}...`); 197 + logDebug(`WebSocket state when receiving: ${ws.readyState}`); 134 198 const message = JSON.parse(event.data); 135 199 136 200 if (message.type === "changes" && Array.isArray(message.data)) { ··· 196 260 197 261 // Send changes to the server 198 262 async function sendChanges(dbname: string) { 263 + logDebug(`Attempting to send changes for ${dbname}`); 264 + 199 265 const connection = connections[dbname]; 200 266 if (!connection) { 201 267 logDebug(`Cannot send changes - no connection for ${dbname}`); ··· 204 270 205 271 if (!connection.ws || connection.ws.readyState !== WebSocket.OPEN) { 206 272 logDebug(`Cannot send changes - WebSocket not open for ${dbname}`); 273 + logDebug(`WebSocket state: ${connection.ws ? connection.ws.readyState : 'null'}`); 207 274 return; 208 275 } 276 + 277 + logDebug(`Connection and WebSocket OK - proceeding with changes`); 209 278 210 279 try { 211 280 logDebug(`Querying for changes since version ${connection.lastSyncVersion}`); ··· 247 316 }; 248 317 }); 249 318 250 - // Send changes to server 319 + // Send changes to server with signature 251 320 logDebug(`Sending ${changes.length} changes to server`); 252 - connection.ws.send(JSON.stringify({ 321 + 322 + // Create the message payload 323 + const timestamp = Date.now(); 324 + const payload = { 253 325 type: "changes", 254 - data: formattedChanges 255 - })); 326 + data: formattedChanges, 327 + timestamp 328 + }; 329 + 330 + logDebug(`Preparing to sign changes for room ${connection.room}`); 331 + 332 + // Workers don't have direct access to localStorage 333 + // We need to request the key from the main thread 334 + logDebug(`Requesting key and signature from main thread for room ${connection.room}`); 335 + 336 + try { 337 + // We don't have the key here - just sending request to main thread 338 + logDebug(`Requesting signature from main thread for ${formattedChanges.length} changes`); 339 + 340 + // Need to get the actual key object for signing 341 + // In a real implementation, we would store the CryptoKey object securely 342 + // For now, we'll post a message to the main thread to request the signature 343 + 344 + self.postMessage({ 345 + type: 'REQUEST_SIGNATURE', 346 + dbname, 347 + payload, 348 + room: connection.room 349 + }); 350 + 351 + // The main thread will handle the signature and send the message 352 + // This is temporary until we implement proper key storage in the worker 353 + } catch (error) { 354 + logError(`Error preparing signature:`, error); 355 + self.postMessage({ type: 'SYNC_ERROR', dbname, error: 'Failed to prepare signature' }); 356 + } 256 357 257 358 // Update last sync version 258 359 const maxVersion = Math.max(...changes.map(c => Number(c[5])));
+289
server/auth.go
··· 1 + package main 2 + 3 + import ( 4 + "database/sql" 5 + "encoding/json" 6 + "log" 7 + "net/http" 8 + 9 + _ "github.com/mattn/go-sqlite3" 10 + ) 11 + 12 + // KeyPermissions defines the permission levels for a key 13 + type KeyPermissions struct { 14 + Read bool `json:"read"` 15 + Write bool `json:"write"` 16 + Invite bool `json:"invite"` 17 + } 18 + 19 + // KeyRegistrationRequest is the expected format for key registration 20 + type KeyRegistrationRequest struct { 21 + RoomID string `json:"roomId"` 22 + PublicKey string `json:"publicKey"` 23 + Perms KeyPermissions `json:"permissions"` 24 + } 25 + 26 + // KeyRegistrationResponse is the response format for key registration 27 + type KeyRegistrationResponse struct { 28 + Success bool `json:"success"` 29 + Message string `json:"message"` 30 + } 31 + 32 + var authDB *sql.DB 33 + 34 + // InitAuthDB initializes the authentication database 35 + func InitAuthDB() error { 36 + var err error 37 + authDB, err = sql.Open("sqlite3", "./auth.db") 38 + if err != nil { 39 + return err 40 + } 41 + 42 + // Ensure the database connection works 43 + if err = authDB.Ping(); err != nil { 44 + log.Printf("Failed to connect to auth database: %v", err) 45 + return err 46 + } 47 + 48 + log.Printf("Successfully connected to auth database") 49 + 50 + // Create tables if they don't exist 51 + _, err = authDB.Exec(` 52 + CREATE TABLE IF NOT EXISTS rooms ( 53 + room_id TEXT PRIMARY KEY, 54 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 55 + ); 56 + 57 + CREATE INDEX IF NOT EXISTS idx_rooms_room_id ON rooms(room_id); 58 + 59 + CREATE TABLE IF NOT EXISTS room_keys ( 60 + id INTEGER PRIMARY KEY AUTOINCREMENT, 61 + room_id TEXT NOT NULL, 62 + public_key TEXT NOT NULL, 63 + can_read BOOLEAN NOT NULL DEFAULT 1, 64 + can_write BOOLEAN NOT NULL DEFAULT 1, 65 + can_invite BOOLEAN NOT NULL DEFAULT 0, 66 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 67 + UNIQUE(room_id, public_key) 68 + ); 69 + 70 + CREATE INDEX IF NOT EXISTS idx_room_keys_room_id ON room_keys(room_id); 71 + `) 72 + 73 + if err != nil { 74 + log.Printf("Failed to create auth database tables: %v", err) 75 + return err 76 + } 77 + 78 + log.Printf("Auth database tables created successfully") 79 + return nil 80 + } 81 + 82 + // CloseAuthDB closes the authentication database connection 83 + func CloseAuthDB() { 84 + if authDB != nil { 85 + authDB.Close() 86 + } 87 + } 88 + 89 + // HandleKeyRegistration handles the registration of public keys for rooms 90 + func HandleKeyRegistration(w http.ResponseWriter, r *http.Request) { 91 + // Set CORS headers 92 + w.Header().Set("Access-Control-Allow-Origin", "*") 93 + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") 94 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 95 + 96 + // Handle preflight OPTIONS request 97 + if r.Method == http.MethodOptions { 98 + log.Println("Received OPTIONS request for key registration") 99 + w.WriteHeader(http.StatusOK) 100 + return 101 + } 102 + 103 + // Only allow POST requests 104 + if r.Method != http.MethodPost { 105 + log.Printf("Received non-POST request for key registration: %s", r.Method) 106 + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 107 + return 108 + } 109 + 110 + log.Println("Received POST request for key registration") 111 + 112 + // Parse the request body 113 + var req KeyRegistrationRequest 114 + decoder := json.NewDecoder(r.Body) 115 + if err := decoder.Decode(&req); err != nil { 116 + log.Printf("Error decoding key registration request: %v", err) 117 + http.Error(w, "Invalid request format", http.StatusBadRequest) 118 + return 119 + } 120 + 121 + log.Printf("Request content: Room ID=%s, Public Key length=%d", req.RoomID, len(req.PublicKey)) 122 + 123 + // Validate the request 124 + if req.RoomID == "" || req.PublicKey == "" { 125 + http.Error(w, "Room ID and public key are required", http.StatusBadRequest) 126 + return 127 + } 128 + 129 + // Set default permissions if not provided 130 + if req.Perms == (KeyPermissions{}) { 131 + req.Perms = KeyPermissions{ 132 + Read: true, 133 + Write: true, 134 + Invite: false, 135 + } 136 + } 137 + 138 + // Store the key in the database 139 + log.Printf("Storing key in database - Room: %s, Perms: read=%v, write=%v, invite=%v", 140 + req.RoomID, req.Perms.Read, req.Perms.Write, req.Perms.Invite) 141 + 142 + _, err := authDB.Exec( 143 + `INSERT OR REPLACE INTO room_keys 144 + (room_id, public_key, can_read, can_write, can_invite) 145 + VALUES (?, ?, ?, ?, ?)`, 146 + req.RoomID, req.PublicKey, req.Perms.Read, req.Perms.Write, req.Perms.Invite, 147 + ) 148 + 149 + if err != nil { 150 + log.Printf("Error storing key: %v", err) 151 + http.Error(w, "Failed to store key", http.StatusInternalServerError) 152 + return 153 + } 154 + 155 + // Return success response 156 + response := KeyRegistrationResponse{ 157 + Success: true, 158 + Message: "Key registered successfully", 159 + } 160 + 161 + w.Header().Set("Content-Type", "application/json") 162 + w.WriteHeader(http.StatusOK) 163 + json.NewEncoder(w).Encode(response) 164 + 165 + log.Printf("Key registration successful - Room: %s, PublicKey: %s, Permissions: read=%v, write=%v, invite=%v", 166 + req.RoomID, 167 + req.PublicKey[:20] + "...", // Only log part of the key for security 168 + req.Perms.Read, 169 + req.Perms.Write, 170 + req.Perms.Invite) 171 + } 172 + 173 + // GetRoomKeys retrieves all keys for a specific room 174 + func GetRoomKeys(roomID string) ([]struct { 175 + PublicKey string 176 + CanRead bool 177 + CanWrite bool 178 + CanInvite bool 179 + }, error) { 180 + rows, err := authDB.Query( 181 + `SELECT public_key, can_read, can_write, can_invite 182 + FROM room_keys 183 + WHERE room_id = ?`, 184 + roomID, 185 + ) 186 + if err != nil { 187 + return nil, err 188 + } 189 + defer rows.Close() 190 + 191 + var keys []struct { 192 + PublicKey string 193 + CanRead bool 194 + CanWrite bool 195 + CanInvite bool 196 + } 197 + 198 + for rows.Next() { 199 + var key struct { 200 + PublicKey string 201 + CanRead bool 202 + CanWrite bool 203 + CanInvite bool 204 + } 205 + if err := rows.Scan(&key.PublicKey, &key.CanRead, &key.CanWrite, &key.CanInvite); err != nil { 206 + return nil, err 207 + } 208 + keys = append(keys, key) 209 + } 210 + 211 + return keys, nil 212 + } 213 + 214 + // GetOrCreateRoom gets a room from the auth database or creates it if it doesn't exist 215 + func GetOrCreateRoom(roomID string) error { 216 + // Check if room exists 217 + var exists bool 218 + err := authDB.QueryRow("SELECT 1 FROM rooms WHERE room_id = ?", roomID).Scan(&exists) 219 + if err != nil && err != sql.ErrNoRows { 220 + return err 221 + } 222 + 223 + // If room doesn't exist, create it 224 + if err == sql.ErrNoRows { 225 + _, err = authDB.Exec( 226 + "INSERT INTO rooms (room_id) VALUES (?)", 227 + roomID, 228 + ) 229 + if err != nil { 230 + return err 231 + } 232 + log.Printf("Created new room in auth database: %s", roomID) 233 + } 234 + 235 + return nil 236 + } 237 + 238 + // CheckRoomExists checks if a room exists in the database 239 + func CheckRoomExists(roomID string) (bool, error) { 240 + var exists bool 241 + err := authDB.QueryRow("SELECT 1 FROM rooms WHERE room_id = ?", roomID).Scan(&exists) 242 + if err == sql.ErrNoRows { 243 + return false, nil 244 + } 245 + if err != nil { 246 + return false, err 247 + } 248 + 249 + return true, nil 250 + } 251 + 252 + // CheckKeyPermission checks if a key has a specific permission for a room 253 + func CheckKeyPermission(roomID, publicKey string, permType string) (bool, error) { 254 + var hasPerm bool 255 + var query string 256 + 257 + switch permType { 258 + case "read": 259 + query = "SELECT can_read FROM room_keys WHERE room_id = ? AND public_key = ?" 260 + case "write": 261 + query = "SELECT can_write FROM room_keys WHERE room_id = ? AND public_key = ?" 262 + case "invite": 263 + query = "SELECT can_invite FROM room_keys WHERE room_id = ? AND public_key = ?" 264 + default: 265 + return false, nil 266 + } 267 + 268 + err := authDB.QueryRow(query, roomID, publicKey).Scan(&hasPerm) 269 + if err == sql.ErrNoRows { 270 + return false, nil 271 + } 272 + if err != nil { 273 + return false, err 274 + } 275 + 276 + return hasPerm, nil 277 + } 278 + 279 + // VerifySignature verifies that a signature was made by the public key 280 + func VerifySignature(publicKey string, data string, signature string) (bool, error) { 281 + // This is a placeholder - the actual implementation will depend on how you're handling 282 + // Web Crypto signatures on the client side 283 + // You'll need to parse the public key, decode the signature, and verify using the appropriate 284 + // crypto algorithm (likely ECDSA or RSA) 285 + 286 + // For now, we'll just return true 287 + return true, nil 288 + } 289 +
+153
server/main.go
··· 6 6 "encoding/json" 7 7 "log" 8 8 "net/http" 9 + "os" 9 10 "sync" 10 11 "time" 11 12 ··· 14 15 ) 15 16 16 17 var upgrader = websocket.Upgrader{ 18 + // Allow all cross-origin connections 17 19 CheckOrigin: func(r *http.Request) bool { 18 20 return true 19 21 }, ··· 101 103 } 102 104 103 105 func handleWebSocket(w http.ResponseWriter, r *http.Request) { 106 + // Set CORS headers for the WebSocket handshake 107 + w.Header().Set("Access-Control-Allow-Origin", "*") 108 + 104 109 // Extract room ID from query parameters 105 110 roomID := r.URL.Query().Get("room") 106 111 if roomID == "" { ··· 108 113 return 109 114 } 110 115 116 + // Get or create the room in the auth database 117 + err := GetOrCreateRoom(roomID) 118 + if err != nil { 119 + log.Printf("Error with auth database for room %s: %v", roomID, err) 120 + http.Error(w, "Server error", http.StatusInternalServerError) 121 + return 122 + } 123 + 111 124 conn, err := upgrader.Upgrade(w, r, nil) 112 125 if err != nil { 113 126 log.Println("Error upgrading connection:", err) ··· 179 192 180 193 case "changes": 181 194 // Client is sending changes 195 + log.Printf("Received changes from client in room %s", roomID) 196 + if publicKey, hasKey := msg["publicKey"].(string); hasKey { 197 + log.Printf("Changes are authenticated with public key: %s...", publicKey[:20]) 198 + } 199 + 182 200 if data, ok := msg["data"].([]interface{}); ok { 201 + log.Printf("Processing %d changes from client", len(data)) 183 202 applyChangesToDB(db, data) 184 203 185 204 // Broadcast changes to other clients in the same room ··· 347 366 return base64.StdEncoding.DecodeString(encoded) 348 367 } 349 368 369 + // createDirIfNotExists creates a directory if it doesn't already exist 370 + func createDirIfNotExists(path string) error { 371 + // Check if the directory exists 372 + if _, err := os.Stat(path); os.IsNotExist(err) { 373 + // Directory does not exist, create it 374 + return os.MkdirAll(path, 0755) 375 + } else if err != nil { 376 + // Some other error occurred 377 + return err 378 + } 379 + // Directory already exists 380 + return nil 381 + } 382 + 383 + 384 + // AuthRequest is the structure for authentication verification requests 385 + type AuthRequest struct { 386 + RoomID string `json:"roomId"` 387 + PublicKey string `json:"publicKey"` 388 + Data string `json:"data"` 389 + Signature string `json:"signature"` 390 + } 391 + 392 + // AuthResponse is the structure for authentication verification responses 393 + type AuthResponse struct { 394 + Authenticated bool `json:"authenticated"` 395 + Message string `json:"message"` 396 + } 397 + 398 + // handleAuthVerify handles authentication verification requests 399 + func handleAuthVerify(w http.ResponseWriter, r *http.Request) { 400 + // Set CORS headers 401 + w.Header().Set("Access-Control-Allow-Origin", "*") 402 + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") 403 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 404 + 405 + // Handle preflight OPTIONS request 406 + if r.Method == http.MethodOptions { 407 + w.WriteHeader(http.StatusOK) 408 + return 409 + } 410 + 411 + // Only allow POST requests 412 + if r.Method != http.MethodPost { 413 + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 414 + return 415 + } 416 + 417 + // Parse the request body 418 + var req AuthRequest 419 + decoder := json.NewDecoder(r.Body) 420 + if err := decoder.Decode(&req); err != nil { 421 + log.Printf("Error decoding auth request: %v", err) 422 + http.Error(w, "Invalid request format", http.StatusBadRequest) 423 + return 424 + } 425 + 426 + // Validate the request 427 + if req.RoomID == "" || req.PublicKey == "" || req.Signature == "" || req.Data == "" { 428 + http.Error(w, "Room ID, public key, data, and signature are required", http.StatusBadRequest) 429 + return 430 + } 431 + 432 + // Verify the signature 433 + isValid, err := VerifySignature(req.PublicKey, req.Data, req.Signature) 434 + if err != nil { 435 + log.Printf("Error verifying signature: %v", err) 436 + http.Error(w, "Failed to verify signature", http.StatusInternalServerError) 437 + return 438 + } 439 + 440 + if !isValid { 441 + // Return failure response 442 + response := AuthResponse{ 443 + Authenticated: false, 444 + Message: "Signature verification failed", 445 + } 446 + 447 + w.Header().Set("Content-Type", "application/json") 448 + w.WriteHeader(http.StatusUnauthorized) 449 + json.NewEncoder(w).Encode(response) 450 + return 451 + } 452 + 453 + // Check if the public key has write permission for the room 454 + hasWritePerm, err := CheckKeyPermission(req.RoomID, req.PublicKey, "write") 455 + if err != nil { 456 + log.Printf("Error checking key permission: %v", err) 457 + http.Error(w, "Failed to check permissions", http.StatusInternalServerError) 458 + return 459 + } 460 + 461 + if !hasWritePerm { 462 + // Return failure response 463 + response := AuthResponse{ 464 + Authenticated: false, 465 + Message: "Public key doesn't have write permission for this room", 466 + } 467 + 468 + w.Header().Set("Content-Type", "application/json") 469 + w.WriteHeader(http.StatusForbidden) 470 + json.NewEncoder(w).Encode(response) 471 + return 472 + } 473 + 474 + // Return success response 475 + response := AuthResponse{ 476 + Authenticated: true, 477 + Message: "Authentication successful", 478 + } 479 + 480 + w.Header().Set("Content-Type", "application/json") 481 + w.WriteHeader(http.StatusOK) 482 + json.NewEncoder(w).Encode(response) 483 + } 484 + 350 485 func main() { 351 486 // Register SQLite with CR-SQLite extension 352 487 sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{ 353 488 Extensions: []string{"../db/crsqlite"}, 354 489 }) 355 490 491 + // Initialize the auth database 492 + log.Println("Initializing authentication database...") 493 + if err := InitAuthDB(); err != nil { 494 + log.Printf("Warning: Failed to initialize auth database: %v", err) 495 + } else { 496 + log.Println("Authentication database initialized successfully") 497 + } 498 + // Close auth database connection when the server exits 499 + defer CloseAuthDB() 500 + 356 501 // Create directory for room databases 502 + if err := createDirIfNotExists("./rooms"); err != nil { 503 + log.Printf("Warning: Failed to create rooms directory: %v", err) 504 + } else { 505 + log.Println("Rooms directory created or verified") 506 + } 507 + 357 508 http.HandleFunc("/sync", handleWebSocket) 509 + http.HandleFunc("/auth/verify", handleAuthVerify) 510 + http.HandleFunc("/auth/register-key", HandleKeyRegistration) 358 511 359 512 log.Println("WebSocket server started on :8080") 360 513 log.Fatal(http.ListenAndServe(":8080", nil))