···11+CREATE TABLE IF NOT EXISTS rooms (
22+ room_id TEXT PRIMARY KEY,
33+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
44+);
55+66+CREATE INDEX IF NOT EXISTS idx_rooms_room_id ON rooms(room_id);
···1414import SyncWorker from './worker/sync-worker.ts?worker';
1515const syncWorker = new SyncWorker();
16161717+// Store active crypto key pairs (in memory for the session)
1818+const activeKeyPairs: Record<string, CryptoKeyPair> = {};
1919+2020+// Function to generate a WebCrypto key pair
2121+async function generateKeyPair() {
2222+ try {
2323+ const keyPair = await window.crypto.subtle.generateKey(
2424+ {
2525+ name: "ECDSA",
2626+ namedCurve: "P-256", // Using P-256 curve for good security/performance balance
2727+ },
2828+ true, // extractable
2929+ ["sign", "verify"] // operations allowed with this key
3030+ );
3131+3232+ // Export the public key to store/transmit it
3333+ const publicKeyBuffer = await window.crypto.subtle.exportKey(
3434+ "spki", // Standard format for public keys
3535+ keyPair.publicKey
3636+ );
3737+3838+ // Convert to base64 for storage/transmission
3939+ const publicKeyBase64 = btoa(
4040+ String.fromCharCode.apply(null, new Uint8Array(publicKeyBuffer))
4141+ );
4242+4343+ console.log("KeyPair: " + publicKeyBase64)
4444+4545+ return {
4646+ keyPair,
4747+ publicKeyBase64
4848+ };
4949+ } catch (error) {
5050+ console.error("Error generating key pair:", error);
5151+ throw error;
5252+ }
5353+}
5454+1755// Function to get room ID from URL hash or generate a new one
1818-function getRoomId(): string {
5656+async function getRoomId(): Promise<{ roomId: string, keyPair?: CryptoKeyPair, publicKeyBase64?: string }> {
1957 // Check URL hash for room parameter
2058 const hash = window.location.hash.substring(1);
2159 const params = new URLSearchParams(hash);
2260 const roomFromHash = params.get('room');
23612462 if (roomFromHash) {
2525- return roomFromHash;
6363+ // Room exists in URL, check if we have keys for it in localStorage
6464+ const storedKeys = localStorage.getItem(`keys-${roomFromHash}`);
6565+ if (storedKeys) {
6666+ // We have keys for this room, parse and log them
6767+ const keyData = JSON.parse(storedKeys);
6868+ console.log(`Loading existing keys for room: ${roomFromHash}`);
6969+ console.log(`Using existing public key: ${keyData.publicKey.substring(0, 20)}... (created: ${keyData.created})`);
7070+7171+ // Load the key into memory for signing - for now, we just return the public key
7272+ // In a full implementation, we would securely retrieve the private key as well
7373+ try {
7474+ console.log("Found existing key for room");
7575+ // Note that we're not returning a keyPair here since we don't have the private key
7676+ // The calling code will handle this case by just registering the public key
7777+ return {
7878+ roomId: roomFromHash,
7979+ publicKeyBase64: keyData.publicKey
8080+ };
8181+ } catch (error) {
8282+ console.error("Error processing existing key:", error);
8383+ return { roomId: roomFromHash };
8484+ }
8585+ }
8686+8787+ // We have a room from URL but no keys - generate keys for this existing room
8888+ console.log("Room found in URL but no keys - generating new keys");
8989+ const { keyPair, publicKeyBase64 } = await generateKeyPair();
9090+9191+ // Store public key in localStorage
9292+ localStorage.setItem(`keys-${roomFromHash}`, JSON.stringify({
9393+ publicKey: publicKeyBase64,
9494+ created: new Date().toISOString()
9595+ }));
9696+9797+ return {
9898+ roomId: roomFromHash,
9999+ keyPair,
100100+ publicKeyBase64
101101+ };
26102 }
271032828- // Check localStorage
104104+ // Check localStorage for room
29105 const storedRoom = localStorage.getItem("room");
30106 if (storedRoom) {
3131- return storedRoom;
107107+ // Room exists in localStorage, check if we have keys for it
108108+ const storedKeys = localStorage.getItem(`keys-${storedRoom}`);
109109+ if (storedKeys) {
110110+ // We have keys for this room, parse and log them
111111+ const keyData = JSON.parse(storedKeys);
112112+ console.log(`Loading existing keys for room from localStorage: ${storedRoom}`);
113113+ console.log(`Using existing public key: ${keyData.publicKey.substring(0, 20)}... (created: ${keyData.created})`);
114114+115115+ // Load the key into memory for signing - for now, we just return the public key
116116+ try {
117117+ console.log("Found existing key in localStorage");
118118+ // Note that we're not returning a keyPair here since we don't have the private key
119119+ // The calling code will handle this case by just registering the public key
120120+ return {
121121+ roomId: storedRoom,
122122+ publicKeyBase64: keyData.publicKey
123123+ };
124124+ } catch (error) {
125125+ console.error("Error processing existing key from localStorage:", error);
126126+ return { roomId: storedRoom };
127127+ }
128128+ }
129129+130130+ // We have a room in localStorage but no keys - generate keys
131131+ console.log("Room found in localStorage but no keys - generating new keys");
132132+ const { keyPair, publicKeyBase64 } = await generateKeyPair();
133133+134134+ // Store public key in localStorage
135135+ localStorage.setItem(`keys-${storedRoom}`, JSON.stringify({
136136+ publicKey: publicKeyBase64,
137137+ created: new Date().toISOString()
138138+ }));
139139+140140+ return {
141141+ roomId: storedRoom,
142142+ keyPair,
143143+ publicKeyBase64
144144+ };
32145 }
331463434- // Generate a new room ID if none exists
147147+ // Generate a new room ID and key pair
35148 const newRoomId = crypto.randomUUID().replaceAll("-", "");
36149 localStorage.setItem("room", newRoomId);
150150+151151+ // Generate a new key pair for this room
152152+ const { keyPair, publicKeyBase64 } = await generateKeyPair();
153153+154154+ // Store public key in localStorage
155155+ localStorage.setItem(`keys-${newRoomId}`, JSON.stringify({
156156+ publicKey: publicKeyBase64,
157157+ created: new Date().toISOString()
158158+ }));
3715938160 // Update URL with the new room ID
39161 updateUrlWithRoom(newRoomId);
162162+163163+ console.log("KeyPair: " + publicKeyBase64)
401644141- return newRoomId;
165165+ return {
166166+ roomId: newRoomId,
167167+ keyPair,
168168+ publicKeyBase64
169169+ };
42170}
431714444-// Function to update URL with room ID
172172+45173function updateUrlWithRoom(roomId: string) {
46174 const hash = window.location.hash.substring(1);
47175 const params = new URLSearchParams(hash);
···66194const initDb = async () => {
67195 const sqlite = await initWasm(() => wasmUrl);
681966969- // Get or create room ID
7070- const roomId = getRoomId();
197197+ // Get or create room ID and keys
198198+ const { roomId, keyPair, publicKeyBase64 } = await getRoomId();
199199+200200+ // In this case, we've already got the publicKeyBase64 from getRoomId
201201+ // so we can skip the extraction of existing keys
202202+203203+ // If we have a key pair from generation, store it in memory
204204+ if (keyPair) {
205205+ console.log("Generated new key pair for room:", roomId);
206206+ console.log("Public key:", publicKeyBase64);
207207+208208+ // Store key pair in memory for the session
209209+ activeKeyPairs[roomId] = keyPair;
210210+ } else if (publicKeyBase64) {
211211+ console.log("Using existing public key for room:", roomId);
212212+ console.log("Public key:", publicKeyBase64.substring(0, 20) + "...");
213213+214214+ // Generate a new key pair for this session
215215+ console.log("Generating new key pair for existing public key");
216216+ try {
217217+ const { keyPair: newKeyPair } = await generateKeyPair();
218218+ console.log("Generated new key pair for signing during this session");
219219+220220+ // Store the new key pair in memory for this session
221221+ activeKeyPairs[roomId] = newKeyPair;
222222+ } catch (error) {
223223+ console.error("Error generating new key pair for signing:", error);
224224+ }
225225+ }
226226+227227+ // Register the key with the server (either new or existing)
228228+ if (publicKeyBase64) {
229229+ await registerKeyWithServer(roomId, publicKeyBase64);
230230+ }
231231+232232+ // Force an initial sync after registration
233233+ console.log("Triggering initial sync...");
7123472235 // Use room ID in database name to isolate data per room
73236 const dbname = `todo-${roomId}.db`;
···102265 const rx = tblrx(db);
103266 return { db, rx, roomId, dbname };
104267};
268268+269269+// Function to register a key with the server
270270+async function registerKeyWithServer(roomId: string, publicKey: string): Promise<boolean> {
271271+ try {
272272+ console.log("Registering public key with server for room:", roomId);
273273+ console.log("Public key to register:", publicKey.substring(0, 20) + "...");
274274+275275+ const requestBody = {
276276+ roomId: roomId,
277277+ publicKey: publicKey,
278278+ permissions: {
279279+ read: true,
280280+ write: true,
281281+ invite: true
282282+ }
283283+ };
284284+285285+ console.log("Request body:", JSON.stringify(requestBody));
286286+287287+ const response = await fetch('http://localhost:8080/auth/register-key', {
288288+ method: 'POST',
289289+ headers: {
290290+ 'Content-Type': 'application/json',
291291+ 'Accept': 'application/json',
292292+ },
293293+ mode: 'cors',
294294+ credentials: 'omit',
295295+ body: JSON.stringify(requestBody),
296296+ });
297297+298298+ const result = await response.json();
299299+ console.log("Key registration response:", result);
300300+301301+ if (!response.ok) {
302302+ console.error("Failed to register public key with server:", result.message);
303303+ return false;
304304+ } else {
305305+ console.log("Successfully registered public key with server");
306306+ return true;
307307+ }
308308+ } catch (error) {
309309+ console.error("Error registering public key:", error);
310310+ return false;
311311+ }
312312+}
313313+314314+// Function to sign a payload using a room's key pair
315315+async function signPayload(roomId: string, payload: any): Promise<{signature: string, publicKey: string} | null> {
316316+ console.log(`Signing payload for room ${roomId}`);
317317+318318+ // Check if we have the key pair in memory
319319+ const keyPair = activeKeyPairs[roomId];
320320+ if (!keyPair) {
321321+ console.error(`No key pair found in memory for room ${roomId}`);
322322+ console.log("Attempting to regenerate key pair from storage...");
323323+324324+ // For real implementation, we would need to securely store/retrieve the private key
325325+ // This is just a placeholder for demonstration
326326+ console.error("Cannot sign without private key - please reload the page to generate a new keypair");
327327+ return null;
328328+ }
329329+330330+ try {
331331+ // Get the public key
332332+ const storedKeys = localStorage.getItem(`keys-${roomId}`);
333333+ if (!storedKeys) {
334334+ console.error(`No stored keys found for room ${roomId}`);
335335+ return null;
336336+ }
337337+338338+ const keyData = JSON.parse(storedKeys);
339339+ const publicKeyBase64 = keyData.publicKey;
340340+ console.log(`Using public key: ${publicKeyBase64.substring(0, 20)}...`);
341341+342342+ // Stringify and encode the payload
343343+ const payloadString = JSON.stringify(payload);
344344+ const payloadBytes = new TextEncoder().encode(payloadString);
345345+ console.log(`Payload to sign (${payloadBytes.length} bytes): ${payloadString.substring(0, 50)}...`);
346346+347347+ // Sign the payload
348348+ console.log('Signing payload with ECDSA/SHA-256...');
349349+ const signature = await window.crypto.subtle.sign(
350350+ {
351351+ name: "ECDSA",
352352+ hash: {name: "SHA-256"},
353353+ },
354354+ keyPair.privateKey,
355355+ payloadBytes
356356+ );
357357+358358+ // Convert signature to base64
359359+ const signatureArray = new Uint8Array(signature);
360360+ const signatureBase64 = btoa(String.fromCharCode.apply(null, signatureArray));
361361+ console.log(`Generated signature (${signatureArray.length} bytes): ${signatureBase64.substring(0, 20)}...`);
362362+363363+ return {
364364+ signature: signatureBase64,
365365+ publicKey: publicKeyBase64
366366+ };
367367+ } catch (error) {
368368+ console.error("Error signing payload:", error);
369369+ return null;
370370+ }
371371+}
372372+373373+// Handle worker messages
374374+syncWorker.addEventListener('message', async (event) => {
375375+ const { type, dbname, payload, room } = event.data;
376376+377377+ if (type === 'REQUEST_SIGNATURE') {
378378+ console.log('Received request for signature', { dbname, room });
379379+380380+ // Sign the payload
381381+ const signatureData = await signPayload(room, payload);
382382+383383+ if (!signatureData) {
384384+ console.error(`Failed to sign payload for room ${room}`);
385385+ return;
386386+ }
387387+388388+ // Create the authenticated message
389389+ const authMessage = {
390390+ ...payload,
391391+ publicKey: signatureData.publicKey,
392392+ signature: signatureData.signature
393393+ };
394394+395395+ // We don't need to create a new WebSocket connection here
396396+ // The worker already has an open connection
397397+398398+ // Find the WebSocket instance - this is a simplified approach
399399+ // In a real implementation, we would have a more robust way to get the WebSocket
400400+ setTimeout(() => {
401401+ // Send the message to the worker which will forward it to the server
402402+ syncWorker.postMessage({
403403+ type: 'SEND_SIGNED_MESSAGE',
404404+ dbname,
405405+ message: authMessage
406406+ });
407407+ }, 0);
408408+ }
409409+});
105410106411const init = async () => {
107412 const ctx = await initDb();
+109-8
mast-react-vite/src/worker/sync-worker.ts
···26262727// Handle messages from the main thread
2828self.onmessage = async (event) => {
2929- const { type, dbname, config } = event.data;
2929+ const { type, dbname, config, message } = event.data;
3030 logDebug(`Received message: ${type}`, { dbname, config });
31313232 switch (type) {
···3838 break;
3939 case 'SYNC_CHANGES':
4040 // This is triggered when React tells us changes happened
4141- await sendChanges(dbname);
4141+ logDebug(`Received explicit sync request for ${dbname}`);
4242+ try {
4343+ await sendChanges(dbname);
4444+ logDebug(`Manual sync completed for ${dbname}`);
4545+ } catch (error) {
4646+ logError(`Error during manual sync: ${error}`);
4747+ }
4848+ break;
4949+ case 'SEND_SIGNED_MESSAGE':
5050+ // Send a pre-signed message to the server
5151+ await sendSignedMessage(dbname, message);
4252 break;
4353 }
4454};
45555656+// Function to send a pre-signed message to the server
5757+async function sendSignedMessage(dbname: string, message: any) {
5858+ const connection = connections[dbname];
5959+ if (!connection) {
6060+ logDebug(`Cannot send signed message - no connection for ${dbname}`);
6161+ return;
6262+ }
6363+6464+ if (!connection.ws || connection.ws.readyState !== WebSocket.OPEN) {
6565+ logDebug(`Cannot send signed message - WebSocket not open for ${dbname}`);
6666+ return;
6767+ }
6868+6969+ try {
7070+ logDebug(`Sending signed message for ${dbname}`);
7171+ logDebug(`Message type: ${message.type}, with data count: ${message.data?.length || 0}`);
7272+ if (message.publicKey) {
7373+ logDebug(`Authenticated with public key: ${message.publicKey.substring(0, 20)}... and signature length: ${message.signature?.length || 0}`);
7474+ }
7575+ connection.ws.send(JSON.stringify(message));
7676+7777+ // If this was a changes message, update the last sync version
7878+ if (message.type === "changes" && Array.isArray(message.data) && message.data.length > 0) {
7979+ logDebug(`Sending ${message.data.length} changes to server via WebSocket`);
8080+8181+ // If the message is authenticated, log that
8282+ if (message.publicKey && message.signature) {
8383+ logDebug(`Message is authenticated with public key: ${message.publicKey.substring(0, 20)}... and signature`);
8484+ } else {
8585+ logDebug(`Message is NOT authenticated - no signature attached`);
8686+ }
8787+8888+ const versions = message.data.map((change: any) => Number(change.DBVersion));
8989+ const maxVersion = Math.max(...versions);
9090+ if (maxVersion > connection.lastSyncVersion) {
9191+ connection.lastSyncVersion = maxVersion;
9292+ logDebug(`Updated lastSyncVersion to ${maxVersion}`);
9393+ }
9494+9595+ // Notify main thread that changes were sent
9696+ self.postMessage({ type: 'CHANGES_SENT', dbname, count: message.data.length });
9797+ }
9898+ } catch (error) {
9999+ logError(`Error sending signed message:`, error);
100100+ self.postMessage({ type: 'SYNC_ERROR', dbname, error: 'Failed to send signed message' });
101101+ }
102102+};
103103+46104// Start syncing a database
47105async function startSync(dbname: string, config: { room: string, url: string }) {
48106 try {
···108166109167 // Set up WebSocket event handlers
110168 ws.onopen = () => {
111111- logDebug(`WebSocket connected for ${dbname}`);
169169+ logDebug(`WebSocket connected for ${dbname} (state: ${ws.readyState})`);
112170 connections[dbname].isConnecting = false;
113171114172 // Set up change listener using onUpdate
115173 db.onUpdate(async () => {
116174 logDebug(`Database update detected for ${dbname}`);
117117- await sendChanges(dbname);
175175+ logDebug(`Sending changes to server for ${dbname}...`);
176176+177177+ // Add a small delay to ensure the changes are committed
178178+ setTimeout(async () => {
179179+ await sendChanges(dbname);
180180+ }, 100);
118181 });
119182120183 // Initial sync - request changes from server
···131194 ws.onmessage = async (event) => {
132195 try {
133196 logDebug(`Received WebSocket message: ${event.data.substring(0, 100)}...`);
197197+ logDebug(`WebSocket state when receiving: ${ws.readyState}`);
134198 const message = JSON.parse(event.data);
135199136200 if (message.type === "changes" && Array.isArray(message.data)) {
···196260197261// Send changes to the server
198262async function sendChanges(dbname: string) {
263263+ logDebug(`Attempting to send changes for ${dbname}`);
264264+199265 const connection = connections[dbname];
200266 if (!connection) {
201267 logDebug(`Cannot send changes - no connection for ${dbname}`);
···204270205271 if (!connection.ws || connection.ws.readyState !== WebSocket.OPEN) {
206272 logDebug(`Cannot send changes - WebSocket not open for ${dbname}`);
273273+ logDebug(`WebSocket state: ${connection.ws ? connection.ws.readyState : 'null'}`);
207274 return;
208275 }
276276+277277+ logDebug(`Connection and WebSocket OK - proceeding with changes`);
209278210279 try {
211280 logDebug(`Querying for changes since version ${connection.lastSyncVersion}`);
···247316 };
248317 });
249318250250- // Send changes to server
319319+ // Send changes to server with signature
251320 logDebug(`Sending ${changes.length} changes to server`);
252252- connection.ws.send(JSON.stringify({
321321+322322+ // Create the message payload
323323+ const timestamp = Date.now();
324324+ const payload = {
253325 type: "changes",
254254- data: formattedChanges
255255- }));
326326+ data: formattedChanges,
327327+ timestamp
328328+ };
329329+330330+ logDebug(`Preparing to sign changes for room ${connection.room}`);
331331+332332+ // Workers don't have direct access to localStorage
333333+ // We need to request the key from the main thread
334334+ logDebug(`Requesting key and signature from main thread for room ${connection.room}`);
335335+336336+ try {
337337+ // We don't have the key here - just sending request to main thread
338338+ logDebug(`Requesting signature from main thread for ${formattedChanges.length} changes`);
339339+340340+ // Need to get the actual key object for signing
341341+ // In a real implementation, we would store the CryptoKey object securely
342342+ // For now, we'll post a message to the main thread to request the signature
343343+344344+ self.postMessage({
345345+ type: 'REQUEST_SIGNATURE',
346346+ dbname,
347347+ payload,
348348+ room: connection.room
349349+ });
350350+351351+ // The main thread will handle the signature and send the message
352352+ // This is temporary until we implement proper key storage in the worker
353353+ } catch (error) {
354354+ logError(`Error preparing signature:`, error);
355355+ self.postMessage({ type: 'SYNC_ERROR', dbname, error: 'Failed to prepare signature' });
356356+ }
256357257358 // Update last sync version
258359 const maxVersion = Math.max(...changes.map(c => Number(c[5])));
+289
server/auth.go
···11+package main
22+33+import (
44+ "database/sql"
55+ "encoding/json"
66+ "log"
77+ "net/http"
88+99+ _ "github.com/mattn/go-sqlite3"
1010+)
1111+1212+// KeyPermissions defines the permission levels for a key
1313+type KeyPermissions struct {
1414+ Read bool `json:"read"`
1515+ Write bool `json:"write"`
1616+ Invite bool `json:"invite"`
1717+}
1818+1919+// KeyRegistrationRequest is the expected format for key registration
2020+type KeyRegistrationRequest struct {
2121+ RoomID string `json:"roomId"`
2222+ PublicKey string `json:"publicKey"`
2323+ Perms KeyPermissions `json:"permissions"`
2424+}
2525+2626+// KeyRegistrationResponse is the response format for key registration
2727+type KeyRegistrationResponse struct {
2828+ Success bool `json:"success"`
2929+ Message string `json:"message"`
3030+}
3131+3232+var authDB *sql.DB
3333+3434+// InitAuthDB initializes the authentication database
3535+func InitAuthDB() error {
3636+ var err error
3737+ authDB, err = sql.Open("sqlite3", "./auth.db")
3838+ if err != nil {
3939+ return err
4040+ }
4141+4242+ // Ensure the database connection works
4343+ if err = authDB.Ping(); err != nil {
4444+ log.Printf("Failed to connect to auth database: %v", err)
4545+ return err
4646+ }
4747+4848+ log.Printf("Successfully connected to auth database")
4949+5050+ // Create tables if they don't exist
5151+ _, err = authDB.Exec(`
5252+ CREATE TABLE IF NOT EXISTS rooms (
5353+ room_id TEXT PRIMARY KEY,
5454+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
5555+ );
5656+5757+ CREATE INDEX IF NOT EXISTS idx_rooms_room_id ON rooms(room_id);
5858+5959+ CREATE TABLE IF NOT EXISTS room_keys (
6060+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6161+ room_id TEXT NOT NULL,
6262+ public_key TEXT NOT NULL,
6363+ can_read BOOLEAN NOT NULL DEFAULT 1,
6464+ can_write BOOLEAN NOT NULL DEFAULT 1,
6565+ can_invite BOOLEAN NOT NULL DEFAULT 0,
6666+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
6767+ UNIQUE(room_id, public_key)
6868+ );
6969+7070+ CREATE INDEX IF NOT EXISTS idx_room_keys_room_id ON room_keys(room_id);
7171+ `)
7272+7373+ if err != nil {
7474+ log.Printf("Failed to create auth database tables: %v", err)
7575+ return err
7676+ }
7777+7878+ log.Printf("Auth database tables created successfully")
7979+ return nil
8080+}
8181+8282+// CloseAuthDB closes the authentication database connection
8383+func CloseAuthDB() {
8484+ if authDB != nil {
8585+ authDB.Close()
8686+ }
8787+}
8888+8989+// HandleKeyRegistration handles the registration of public keys for rooms
9090+func HandleKeyRegistration(w http.ResponseWriter, r *http.Request) {
9191+ // Set CORS headers
9292+ w.Header().Set("Access-Control-Allow-Origin", "*")
9393+ w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
9494+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
9595+9696+ // Handle preflight OPTIONS request
9797+ if r.Method == http.MethodOptions {
9898+ log.Println("Received OPTIONS request for key registration")
9999+ w.WriteHeader(http.StatusOK)
100100+ return
101101+ }
102102+103103+ // Only allow POST requests
104104+ if r.Method != http.MethodPost {
105105+ log.Printf("Received non-POST request for key registration: %s", r.Method)
106106+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
107107+ return
108108+ }
109109+110110+ log.Println("Received POST request for key registration")
111111+112112+ // Parse the request body
113113+ var req KeyRegistrationRequest
114114+ decoder := json.NewDecoder(r.Body)
115115+ if err := decoder.Decode(&req); err != nil {
116116+ log.Printf("Error decoding key registration request: %v", err)
117117+ http.Error(w, "Invalid request format", http.StatusBadRequest)
118118+ return
119119+ }
120120+121121+ log.Printf("Request content: Room ID=%s, Public Key length=%d", req.RoomID, len(req.PublicKey))
122122+123123+ // Validate the request
124124+ if req.RoomID == "" || req.PublicKey == "" {
125125+ http.Error(w, "Room ID and public key are required", http.StatusBadRequest)
126126+ return
127127+ }
128128+129129+ // Set default permissions if not provided
130130+ if req.Perms == (KeyPermissions{}) {
131131+ req.Perms = KeyPermissions{
132132+ Read: true,
133133+ Write: true,
134134+ Invite: false,
135135+ }
136136+ }
137137+138138+ // Store the key in the database
139139+ log.Printf("Storing key in database - Room: %s, Perms: read=%v, write=%v, invite=%v",
140140+ req.RoomID, req.Perms.Read, req.Perms.Write, req.Perms.Invite)
141141+142142+ _, err := authDB.Exec(
143143+ `INSERT OR REPLACE INTO room_keys
144144+ (room_id, public_key, can_read, can_write, can_invite)
145145+ VALUES (?, ?, ?, ?, ?)`,
146146+ req.RoomID, req.PublicKey, req.Perms.Read, req.Perms.Write, req.Perms.Invite,
147147+ )
148148+149149+ if err != nil {
150150+ log.Printf("Error storing key: %v", err)
151151+ http.Error(w, "Failed to store key", http.StatusInternalServerError)
152152+ return
153153+ }
154154+155155+ // Return success response
156156+ response := KeyRegistrationResponse{
157157+ Success: true,
158158+ Message: "Key registered successfully",
159159+ }
160160+161161+ w.Header().Set("Content-Type", "application/json")
162162+ w.WriteHeader(http.StatusOK)
163163+ json.NewEncoder(w).Encode(response)
164164+165165+ log.Printf("Key registration successful - Room: %s, PublicKey: %s, Permissions: read=%v, write=%v, invite=%v",
166166+ req.RoomID,
167167+ req.PublicKey[:20] + "...", // Only log part of the key for security
168168+ req.Perms.Read,
169169+ req.Perms.Write,
170170+ req.Perms.Invite)
171171+}
172172+173173+// GetRoomKeys retrieves all keys for a specific room
174174+func GetRoomKeys(roomID string) ([]struct {
175175+ PublicKey string
176176+ CanRead bool
177177+ CanWrite bool
178178+ CanInvite bool
179179+}, error) {
180180+ rows, err := authDB.Query(
181181+ `SELECT public_key, can_read, can_write, can_invite
182182+ FROM room_keys
183183+ WHERE room_id = ?`,
184184+ roomID,
185185+ )
186186+ if err != nil {
187187+ return nil, err
188188+ }
189189+ defer rows.Close()
190190+191191+ var keys []struct {
192192+ PublicKey string
193193+ CanRead bool
194194+ CanWrite bool
195195+ CanInvite bool
196196+ }
197197+198198+ for rows.Next() {
199199+ var key struct {
200200+ PublicKey string
201201+ CanRead bool
202202+ CanWrite bool
203203+ CanInvite bool
204204+ }
205205+ if err := rows.Scan(&key.PublicKey, &key.CanRead, &key.CanWrite, &key.CanInvite); err != nil {
206206+ return nil, err
207207+ }
208208+ keys = append(keys, key)
209209+ }
210210+211211+ return keys, nil
212212+}
213213+214214+// GetOrCreateRoom gets a room from the auth database or creates it if it doesn't exist
215215+func GetOrCreateRoom(roomID string) error {
216216+ // Check if room exists
217217+ var exists bool
218218+ err := authDB.QueryRow("SELECT 1 FROM rooms WHERE room_id = ?", roomID).Scan(&exists)
219219+ if err != nil && err != sql.ErrNoRows {
220220+ return err
221221+ }
222222+223223+ // If room doesn't exist, create it
224224+ if err == sql.ErrNoRows {
225225+ _, err = authDB.Exec(
226226+ "INSERT INTO rooms (room_id) VALUES (?)",
227227+ roomID,
228228+ )
229229+ if err != nil {
230230+ return err
231231+ }
232232+ log.Printf("Created new room in auth database: %s", roomID)
233233+ }
234234+235235+ return nil
236236+}
237237+238238+// CheckRoomExists checks if a room exists in the database
239239+func CheckRoomExists(roomID string) (bool, error) {
240240+ var exists bool
241241+ err := authDB.QueryRow("SELECT 1 FROM rooms WHERE room_id = ?", roomID).Scan(&exists)
242242+ if err == sql.ErrNoRows {
243243+ return false, nil
244244+ }
245245+ if err != nil {
246246+ return false, err
247247+ }
248248+249249+ return true, nil
250250+}
251251+252252+// CheckKeyPermission checks if a key has a specific permission for a room
253253+func CheckKeyPermission(roomID, publicKey string, permType string) (bool, error) {
254254+ var hasPerm bool
255255+ var query string
256256+257257+ switch permType {
258258+ case "read":
259259+ query = "SELECT can_read FROM room_keys WHERE room_id = ? AND public_key = ?"
260260+ case "write":
261261+ query = "SELECT can_write FROM room_keys WHERE room_id = ? AND public_key = ?"
262262+ case "invite":
263263+ query = "SELECT can_invite FROM room_keys WHERE room_id = ? AND public_key = ?"
264264+ default:
265265+ return false, nil
266266+ }
267267+268268+ err := authDB.QueryRow(query, roomID, publicKey).Scan(&hasPerm)
269269+ if err == sql.ErrNoRows {
270270+ return false, nil
271271+ }
272272+ if err != nil {
273273+ return false, err
274274+ }
275275+276276+ return hasPerm, nil
277277+}
278278+279279+// VerifySignature verifies that a signature was made by the public key
280280+func VerifySignature(publicKey string, data string, signature string) (bool, error) {
281281+ // This is a placeholder - the actual implementation will depend on how you're handling
282282+ // Web Crypto signatures on the client side
283283+ // You'll need to parse the public key, decode the signature, and verify using the appropriate
284284+ // crypto algorithm (likely ECDSA or RSA)
285285+286286+ // For now, we'll just return true
287287+ return true, nil
288288+}
289289+
+153
server/main.go
···66 "encoding/json"
77 "log"
88 "net/http"
99+ "os"
910 "sync"
1011 "time"
1112···1415)
15161617var upgrader = websocket.Upgrader{
1818+ // Allow all cross-origin connections
1719 CheckOrigin: func(r *http.Request) bool {
1820 return true
1921 },
···101103}
102104103105func handleWebSocket(w http.ResponseWriter, r *http.Request) {
106106+ // Set CORS headers for the WebSocket handshake
107107+ w.Header().Set("Access-Control-Allow-Origin", "*")
108108+104109 // Extract room ID from query parameters
105110 roomID := r.URL.Query().Get("room")
106111 if roomID == "" {
···108113 return
109114 }
110115116116+ // Get or create the room in the auth database
117117+ err := GetOrCreateRoom(roomID)
118118+ if err != nil {
119119+ log.Printf("Error with auth database for room %s: %v", roomID, err)
120120+ http.Error(w, "Server error", http.StatusInternalServerError)
121121+ return
122122+ }
123123+111124 conn, err := upgrader.Upgrade(w, r, nil)
112125 if err != nil {
113126 log.Println("Error upgrading connection:", err)
···179192180193 case "changes":
181194 // Client is sending changes
195195+ log.Printf("Received changes from client in room %s", roomID)
196196+ if publicKey, hasKey := msg["publicKey"].(string); hasKey {
197197+ log.Printf("Changes are authenticated with public key: %s...", publicKey[:20])
198198+ }
199199+182200 if data, ok := msg["data"].([]interface{}); ok {
201201+ log.Printf("Processing %d changes from client", len(data))
183202 applyChangesToDB(db, data)
184203185204 // Broadcast changes to other clients in the same room
···347366 return base64.StdEncoding.DecodeString(encoded)
348367}
349368369369+// createDirIfNotExists creates a directory if it doesn't already exist
370370+func createDirIfNotExists(path string) error {
371371+ // Check if the directory exists
372372+ if _, err := os.Stat(path); os.IsNotExist(err) {
373373+ // Directory does not exist, create it
374374+ return os.MkdirAll(path, 0755)
375375+ } else if err != nil {
376376+ // Some other error occurred
377377+ return err
378378+ }
379379+ // Directory already exists
380380+ return nil
381381+}
382382+383383+384384+// AuthRequest is the structure for authentication verification requests
385385+type AuthRequest struct {
386386+ RoomID string `json:"roomId"`
387387+ PublicKey string `json:"publicKey"`
388388+ Data string `json:"data"`
389389+ Signature string `json:"signature"`
390390+}
391391+392392+// AuthResponse is the structure for authentication verification responses
393393+type AuthResponse struct {
394394+ Authenticated bool `json:"authenticated"`
395395+ Message string `json:"message"`
396396+}
397397+398398+// handleAuthVerify handles authentication verification requests
399399+func handleAuthVerify(w http.ResponseWriter, r *http.Request) {
400400+ // Set CORS headers
401401+ w.Header().Set("Access-Control-Allow-Origin", "*")
402402+ w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
403403+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
404404+405405+ // Handle preflight OPTIONS request
406406+ if r.Method == http.MethodOptions {
407407+ w.WriteHeader(http.StatusOK)
408408+ return
409409+ }
410410+411411+ // Only allow POST requests
412412+ if r.Method != http.MethodPost {
413413+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
414414+ return
415415+ }
416416+417417+ // Parse the request body
418418+ var req AuthRequest
419419+ decoder := json.NewDecoder(r.Body)
420420+ if err := decoder.Decode(&req); err != nil {
421421+ log.Printf("Error decoding auth request: %v", err)
422422+ http.Error(w, "Invalid request format", http.StatusBadRequest)
423423+ return
424424+ }
425425+426426+ // Validate the request
427427+ if req.RoomID == "" || req.PublicKey == "" || req.Signature == "" || req.Data == "" {
428428+ http.Error(w, "Room ID, public key, data, and signature are required", http.StatusBadRequest)
429429+ return
430430+ }
431431+432432+ // Verify the signature
433433+ isValid, err := VerifySignature(req.PublicKey, req.Data, req.Signature)
434434+ if err != nil {
435435+ log.Printf("Error verifying signature: %v", err)
436436+ http.Error(w, "Failed to verify signature", http.StatusInternalServerError)
437437+ return
438438+ }
439439+440440+ if !isValid {
441441+ // Return failure response
442442+ response := AuthResponse{
443443+ Authenticated: false,
444444+ Message: "Signature verification failed",
445445+ }
446446+447447+ w.Header().Set("Content-Type", "application/json")
448448+ w.WriteHeader(http.StatusUnauthorized)
449449+ json.NewEncoder(w).Encode(response)
450450+ return
451451+ }
452452+453453+ // Check if the public key has write permission for the room
454454+ hasWritePerm, err := CheckKeyPermission(req.RoomID, req.PublicKey, "write")
455455+ if err != nil {
456456+ log.Printf("Error checking key permission: %v", err)
457457+ http.Error(w, "Failed to check permissions", http.StatusInternalServerError)
458458+ return
459459+ }
460460+461461+ if !hasWritePerm {
462462+ // Return failure response
463463+ response := AuthResponse{
464464+ Authenticated: false,
465465+ Message: "Public key doesn't have write permission for this room",
466466+ }
467467+468468+ w.Header().Set("Content-Type", "application/json")
469469+ w.WriteHeader(http.StatusForbidden)
470470+ json.NewEncoder(w).Encode(response)
471471+ return
472472+ }
473473+474474+ // Return success response
475475+ response := AuthResponse{
476476+ Authenticated: true,
477477+ Message: "Authentication successful",
478478+ }
479479+480480+ w.Header().Set("Content-Type", "application/json")
481481+ w.WriteHeader(http.StatusOK)
482482+ json.NewEncoder(w).Encode(response)
483483+}
484484+350485func main() {
351486 // Register SQLite with CR-SQLite extension
352487 sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{
353488 Extensions: []string{"../db/crsqlite"},
354489 })
355490491491+ // Initialize the auth database
492492+ log.Println("Initializing authentication database...")
493493+ if err := InitAuthDB(); err != nil {
494494+ log.Printf("Warning: Failed to initialize auth database: %v", err)
495495+ } else {
496496+ log.Println("Authentication database initialized successfully")
497497+ }
498498+ // Close auth database connection when the server exits
499499+ defer CloseAuthDB()
500500+356501 // Create directory for room databases
502502+ if err := createDirIfNotExists("./rooms"); err != nil {
503503+ log.Printf("Warning: Failed to create rooms directory: %v", err)
504504+ } else {
505505+ log.Println("Rooms directory created or verified")
506506+ }
507507+357508 http.HandleFunc("/sync", handleWebSocket)
509509+ http.HandleFunc("/auth/verify", handleAuthVerify)
510510+ http.HandleFunc("/auth/register-key", HandleKeyRegistration)
358511359512 log.Println("WebSocket server started on :8080")
360513 log.Fatal(http.ListenAndServe(":8080", nil))