Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/sync/
handler.rs

1//! Sync request handler trait and implementation.
2//!
3//! This module contains transport-agnostic handlers that process
4//! sync requests and generate responses. These handlers can be
5//! used by any transport implementation through the SyncHandler trait.
6
7use std::{
8    collections::{BTreeMap, HashMap},
9    sync::Mutex,
10};
11
12use async_trait::async_trait;
13use tracing::{Instrument, debug, error, info, info_span, trace, warn};
14
15use super::{
16    bootstrap_request_manager::{BootstrapRequest, BootstrapRequestManager, RequestStatus},
17    peer_manager::PeerManager,
18    peer_types::Address,
19    protocol::{
20        BootstrapResponse, HandshakeRequest, HandshakeResponse, IncrementalResponse,
21        PROTOCOL_VERSION, RequestContext, SyncRequest, SyncResponse, SyncTreeRequest,
22    },
23    user_sync_manager::UserSyncManager,
24};
25use crate::{
26    Database, Entry, Error, Instance, Result, WeakInstance,
27    auth::{
28        Permission,
29        crypto::{PublicKey, create_challenge_response, generate_challenge},
30    },
31    crdt::Doc,
32    entry::ID,
33    store::SettingsStore,
34    sync::error::SyncError,
35};
36
37/// Trait for handling sync requests with database access.
38///
39/// Implementations of this trait can process sync requests and generate
40/// appropriate responses, with full access to the database backend for
41/// storing and retrieving entries.
42#[async_trait]
43pub trait SyncHandler: Send + std::marker::Sync {
44    /// Handle a sync request and generate an appropriate response.
45    ///
46    /// This is the main entry point for processing sync messages,
47    /// regardless of which transport they arrived through.
48    ///
49    /// # Arguments
50    /// * `request` - The sync request to process
51    /// * `context` - Context about the request (remote address, etc.)
52    ///
53    /// # Returns
54    /// The appropriate response for the given request.
55    async fn handle_request(&self, request: &SyncRequest, context: &RequestContext)
56    -> SyncResponse;
57}
58
59/// How far a request's timestamp may sit from our clock, in either direction.
60///
61/// Bounds how long a captured signature stays useful. Peers with clocks further
62/// apart than this cannot sync — the window trades clock tolerance against
63/// replay exposure.
64const MAX_REQUEST_AGE_MS: u64 = 60_000;
65
66/// Default implementation of SyncHandler with database backend access.
67pub struct SyncHandlerImpl {
68    instance: WeakInstance,
69    sync_tree_id: ID,
70    /// Nonces spent within the freshness window, keyed by the claiming key.
71    ///
72    /// Makes each signature single-use: without this, anyone who observes a
73    /// signed request can replay it verbatim until it ages out.
74    spent_nonces: Mutex<HashMap<(PublicKey, Vec<u8>), u64>>,
75}
76
77impl SyncHandlerImpl {
78    /// Create a new SyncHandlerImpl with the given instance.
79    ///
80    /// # Arguments
81    /// * `instance` - Database instance for storing and retrieving entries
82    /// * `sync_tree_id` - Root ID of the sync database for storing bootstrap requests
83    pub fn new(instance: Instance, sync_tree_id: ID) -> Self {
84        Self {
85            instance: instance.downgrade(),
86            sync_tree_id,
87            spent_nonces: Mutex::new(HashMap::new()),
88        }
89    }
90
91    /// Authenticate a request that is about to be served data.
92    ///
93    /// Returns the key the caller has proven it holds. Checks, in order:
94    /// the signature covers *this* request to *us*, the request is fresh, and
95    /// its nonce has not been spent.
96    ///
97    /// This does not decide authorization — see [`Database::can_access`].
98    fn authenticate_request(&self, request: &SyncTreeRequest) -> Result<PublicKey> {
99        let auth = request
100            .auth
101            .as_ref()
102            .ok_or_else(|| SyncError::AuthenticationRequired(request.tree_id.to_string()))?;
103
104        let instance = self.instance()?;
105        auth.verify(&instance.id(), &request.tree_id, &request.our_tips)
106            .map_err(|_| {
107                SyncError::AuthenticationFailed("invalid request signature".to_string())
108            })?;
109
110        let now = instance.clock().now_millis();
111        if now.abs_diff(auth.timestamp_ms) > MAX_REQUEST_AGE_MS {
112            return Err(SyncError::AuthenticationFailed(
113                "request timestamp outside the freshness window".to_string(),
114            )
115            .into());
116        }
117
118        // The map holds one entry per verified request in the last window, so
119        // it is bounded by request rate, not by uptime. Cap it if a peer can
120        // ever outrun that.
121        let mut spent = self
122            .spent_nonces
123            .lock()
124            .unwrap_or_else(|poisoned| poisoned.into_inner());
125        spent.retain(|_, spent_at| now.abs_diff(*spent_at) <= MAX_REQUEST_AGE_MS);
126        if spent
127            .insert((auth.key.clone(), auth.nonce.clone()), auth.timestamp_ms)
128            .is_some()
129        {
130            return Err(
131                SyncError::AuthenticationFailed("request nonce already spent".to_string()).into(),
132            );
133        }
134
135        Ok(auth.key.clone())
136    }
137
138    /// Whether this request may be served entries from `tree_id`.
139    ///
140    /// A database with no auth configured, or with a global `*` grant, is
141    /// world-readable by design and needs no credentials. Otherwise the caller
142    /// must prove it holds a key that [`Database::can_access`] accepts —
143    /// directly, through the global grant, or through a delegated tree.
144    async fn authorize_read(&self, request: &SyncTreeRequest) -> Result<()> {
145        if !self.check_if_database_has_auth(&request.tree_id).await? {
146            return Ok(());
147        }
148
149        let key = self.authenticate_request(request)?;
150        if !Database::can_access(&self.instance()?, &request.tree_id, &key, &Permission::Read)
151            .await?
152        {
153            return Err(SyncError::PermissionDenied(format!(
154                "key {key} is not authorized to read {}",
155                request.tree_id
156            ))
157            .into());
158        }
159
160        Ok(())
161    }
162
163    /// Upgrade the weak instance reference to a strong reference.
164    pub(super) fn instance(&self) -> Result<Instance> {
165        self.instance
166            .upgrade()
167            .ok_or_else(|| SyncError::InstanceDropped.into())
168    }
169
170    /// Get access to the sync tree for bootstrap request management.
171    ///
172    /// # Returns
173    /// A Database instance for the sync tree with device key authentication.
174    async fn get_sync_tree(&self) -> Result<Database> {
175        // Load sync tree with the device key
176        let instance = self.instance()?;
177        let signing_key = instance.signing_key()?.clone();
178        Ok(Database::open(&instance, &self.sync_tree_id)
179            .await?
180            .with_key(signing_key))
181    }
182
183    /// Store a bootstrap request in the sync database for manual approval.
184    ///
185    /// # Arguments
186    /// * `tree_id` - ID of the tree being requested
187    /// * `requesting_key` - Public key of the requesting device
188    /// * `requesting_key_name` - Name of the requesting key
189    /// * `requested_permission` - Permission level being requested
190    ///
191    /// # Returns
192    /// The generated UUID for the stored request
193    async fn store_bootstrap_request(
194        &self,
195        tree_id: &ID,
196        requesting_key: &PublicKey,
197        requesting_key_name: &str,
198        requested_permission: &Permission,
199        metadata: Option<Doc>,
200    ) -> Result<String> {
201        let sync_tree = self.get_sync_tree().await?;
202        let txn = sync_tree.new_transaction().await?;
203        let manager = BootstrapRequestManager::new(&txn);
204
205        let request = BootstrapRequest {
206            tree_id: tree_id.clone(),
207            requesting_pubkey: requesting_key.clone(),
208            requesting_key_name: requesting_key_name.to_string(),
209            requested_permission: *requested_permission,
210            timestamp: self.instance()?.clock().now_rfc3339(),
211            status: RequestStatus::Pending,
212            // TODO: We need to get the actual peer address from the transport layer
213            // For now, use a placeholder that will need to be fixed when implementing notifications
214            peer_address: Address {
215                transport_type: "unknown".to_string(),
216                address: "unknown".to_string(),
217            },
218            // TODO(bootstrap-metadata-bound): `metadata` is unbounded, remote-supplied
219            // data persisted to the system sync tree before any approval decision. A
220            // hostile peer can spam large/many pending requests (storage/DoS on the
221            // multi-tenant boundary). Bound the metadata size here, and cap pending
222            // requests per peer, before this is exposed to untrusted peers. Note the
223            // `Doc` is already deserialized at the protocol layer ahead of the
224            // sync-enabled gate, so the size cap ideally belongs there too.
225            metadata,
226        };
227
228        let request_id = manager.store_request(request).await?;
229        txn.commit().await?;
230
231        Ok(request_id)
232    }
233}
234
235#[async_trait]
236impl SyncHandler for SyncHandlerImpl {
237    async fn handle_request(
238        &self,
239        request: &SyncRequest,
240        context: &RequestContext,
241    ) -> SyncResponse {
242        match request {
243            SyncRequest::Handshake(handshake_req) => {
244                debug!("Received handshake request");
245                self.handle_handshake(handshake_req, context).await
246            }
247            SyncRequest::SyncTree(sync_req) => {
248                debug!(tree_id = %sync_req.tree_id, tips_count = sync_req.our_tips.len(), "Received sync tree request");
249                self.handle_sync_tree(sync_req, context).await
250            }
251            SyncRequest::SendEntries(entries) => {
252                // Process and store the received entries
253                let count = entries.len();
254                info!(count = count, "Received entries for synchronization");
255
256                let instance = match self.instance() {
257                    Ok(i) => i,
258                    Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
259                };
260
261                // Group entries by tree_id so we can fire callbacks per-database.
262                // BTreeMap so iteration order is deterministic (sorted by id);
263                // sender order within a tree is preserved by per-tree push order.
264                //
265                // Root entries declare an empty `tree.root` and act as their
266                // own tree_id. Non-root entries always carry a tree_id;
267                // well-formed peers should never send a non-root entry with
268                // `root() == None`. If they do, the entry ends up filed under
269                // its own id and parent-existence checks downstream reject it.
270                let mut by_tree: BTreeMap<ID, Vec<Entry>> = BTreeMap::new();
271                for entry in entries {
272                    let tree_id = entry.root().unwrap_or_else(|| entry.id());
273                    by_tree.entry(tree_id).or_default().push(entry.clone());
274                }
275
276                let mut stored_count = 0usize;
277                for (tree_id, tree_entries) in by_tree {
278                    let batch_size = tree_entries.len();
279                    // Entries arrive over the wire without per-entry signature
280                    // verification; `put_remote_entries` stores them
281                    // Unverified so a future re-verification pass can promote
282                    // them.
283                    match instance.put_remote_entries(&tree_id, tree_entries).await {
284                        Ok(n) => {
285                            stored_count += n;
286                            debug!(tree_id = %tree_id, requested = batch_size, stored = n, "Stored entries");
287                        }
288                        Err(e) => {
289                            error!(tree_id = %tree_id, error = %e, "Failed to store entries batch");
290                        }
291                    }
292                }
293
294                debug!(
295                    received = count,
296                    stored = stored_count,
297                    "Completed entry synchronization"
298                );
299                if count <= 1 {
300                    SyncResponse::Ack
301                } else {
302                    SyncResponse::Count(stored_count)
303                }
304            }
305        }
306    }
307}
308
309impl SyncHandlerImpl {
310    /// Get the highest permission level a key has in the database's auth settings.
311    ///
312    /// This looks up all permissions the key has (direct + global wildcard) and returns
313    /// the highest one. Used for auto-detecting permissions during bootstrap.
314    ///
315    /// # Arguments
316    /// * `tree_id` - The database/tree ID to check auth settings for
317    /// * `requesting_pubkey` - The public key to look up
318    ///
319    /// # Returns
320    /// - `Ok(Some(Permission))` if key has any permissions
321    /// - `Ok(None)` if key not found in auth settings
322    /// - `Err` if database access fails
323    async fn get_key_highest_permission(
324        &self,
325        tree_id: &ID,
326        requesting_pubkey: &PublicKey,
327    ) -> Result<Option<Permission>> {
328        let database = Database::open(&self.instance()?, tree_id).await?;
329        let transaction = database.new_transaction().await?;
330        let settings_store = SettingsStore::new(&transaction)?;
331        let auth_settings = settings_store.auth_snapshot().await?;
332
333        let results = auth_settings.find_all_sigkeys_for_pubkey(requesting_pubkey);
334
335        if results.is_empty() {
336            return Ok(None);
337        }
338
339        // Results are sorted highest first, so take the first one
340        Ok(Some(results[0].1))
341    }
342
343    /// Check that the caller signed this request with `claimed_key`.
344    ///
345    /// `requesting_key` is a name the client picks; it decides which key an
346    /// approval would grant, and on its own it must never unlock data. Anything
347    /// that serves entries on the strength of a claimed key needs this first.
348    fn prove_possession_if_required(
349        &self,
350        request: &SyncTreeRequest,
351        claimed_key: &PublicKey,
352        auth_configured: bool,
353    ) -> Result<()> {
354        if !auth_configured {
355            // World-readable database: nothing is being protected, so there is
356            // nothing to prove.
357            return Ok(());
358        }
359        self.prove_possession(request, claimed_key)
360    }
361
362    /// Check that the caller signed this request with `claimed_key`.
363    fn prove_possession(&self, request: &SyncTreeRequest, claimed_key: &PublicKey) -> Result<()> {
364        let proven = self.authenticate_request(request)?;
365        if proven != *claimed_key {
366            return Err(SyncError::AuthenticationFailed(format!(
367                "request is signed by {proven}, which is not the claimed key {claimed_key}"
368            ))
369            .into());
370        }
371        Ok(())
372    }
373
374    /// Check if the caller holds a key that already has sufficient permissions.
375    ///
376    /// Possession first, then authority. Authority resolves through
377    /// [`Database::can_access`], the pubkey-only access decision, which covers
378    /// direct grants, the global `*` grant, and authority that reaches this
379    /// tree only through a *delegated* tree. Without the delegated case a
380    /// delegated-only key is bounced to manual approval and hangs.
381    ///
382    /// Delegation discovery is **one hop deep**: a key reachable only through a
383    /// chain of delegations still falls through to manual approval. See
384    /// [`Database::can_access`] for why bootstrap searches where entry
385    /// validation walks a named path.
386    ///
387    /// # Returns
388    /// - `Ok(true)` if the caller proved a key with sufficient permission
389    /// - `Ok(false)` if possession failed, or the key lacks permission
390    async fn check_proven_auth_permission(
391        &self,
392        request: &SyncTreeRequest,
393        requesting_pubkey: &PublicKey,
394        requested_permission: &Permission,
395        auth_configured: bool,
396    ) -> Result<bool> {
397        let tree_id = &request.tree_id;
398        if let Err(e) =
399            self.prove_possession_if_required(request, requesting_pubkey, auth_configured)
400        {
401            warn!(
402                tree_id = %tree_id,
403                requesting_pubkey = %requesting_pubkey,
404                error = %e,
405                "Bootstrap key claim not proven - falling back to the approval queue"
406            );
407            return Ok(false);
408        }
409
410        let granted = Database::can_access(
411            &self.instance()?,
412            tree_id,
413            requesting_pubkey,
414            requested_permission,
415        )
416        .await?;
417        if granted {
418            debug!(
419                tree_id = %tree_id,
420                requesting_pubkey = %requesting_pubkey,
421                requested_permission = ?requested_permission,
422                "Key has sufficient permission for bootstrap access"
423            );
424        }
425        Ok(granted)
426    }
427
428    /// Check if a database requires authentication for unauthenticated requests.
429    ///
430    /// This method checks if the database requires authentication for bootstrap requests
431    /// that don't provide credentials. A database allows unauthenticated access if:
432    /// 1. It has no auth settings configured at all (empty auth), OR
433    /// 2. It has a global `*` permission configured that allows unauthenticated access
434    ///
435    /// # Arguments
436    /// * `tree_id` - The database/tree ID to check auth configuration for
437    ///
438    /// # Returns
439    /// - `Ok(true)` if database requires authentication (has auth but no global permission)
440    /// - `Ok(false)` if database allows unauthenticated access (no auth or has global permission)
441    /// - `Err` if the check fails
442    async fn check_if_database_has_auth(&self, tree_id: &ID) -> Result<bool> {
443        let database = Database::open(&self.instance()?, tree_id).await?;
444        let transaction = database.new_transaction().await?;
445        let settings_store = SettingsStore::new(&transaction)?;
446
447        let auth_settings = settings_store.auth_snapshot().await?;
448
449        // Check if auth settings is completely empty (no auth configured)
450        if auth_settings.as_doc().is_empty() {
451            debug!(
452                tree_id = %tree_id,
453                "Database has no auth configured - allowing unauthenticated access"
454            );
455            return Ok(false); // No auth required
456        }
457
458        // Auth is configured - check if there's an Active global permission
459        if let Ok(global_key) = auth_settings.get_global_key()
460            && global_key.is_active()
461        {
462            debug!(
463                tree_id = %tree_id,
464                global_permission = ?global_key.permissions(),
465                "Database has global permission - allowing unauthenticated access"
466            );
467            return Ok(false); // Global permission allows unauthenticated access
468        }
469
470        // Auth is configured but no global permission - require authentication
471        debug!(
472            tree_id = %tree_id,
473            "Database has auth configured without global permission - requiring authentication"
474        );
475        Ok(true) // Auth required
476    }
477
478    /// Check if a database has sync enabled by at least one user.
479    ///
480    /// This is a security-critical check that determines if a database should accept
481    /// any sync requests at all. A database is only eligible for sync if at least one
482    /// user has it in their preferences with `sync_enabled: true`.
483    ///
484    /// # Security
485    /// This method implements fail-closed behavior:
486    /// - Returns `false` on any error (no information leakage)
487    /// - Returns `false` if no users have the database in preferences
488    /// - Returns `false` if combined_settings.sync_enabled is false
489    /// - Only returns `true` if explicitly enabled
490    ///
491    /// # Arguments
492    /// * `tree_id` - The ID of the database to check
493    ///
494    /// # Returns
495    /// `true` if the database has sync enabled, `false` otherwise (including errors)
496    async fn is_database_sync_enabled(&self, tree_id: &ID) -> bool {
497        let instance = match self.instance() {
498            Ok(i) => i,
499            Err(_) => return false, // Fail closed
500        };
501
502        let signing_key = match instance.signing_key() {
503            Ok(k) => k.clone(),
504            Err(_) => return false, // Fail closed
505        };
506
507        let sync_database = match Database::open(&instance, &self.sync_tree_id).await {
508            Ok(db) => db.with_key(signing_key),
509            Err(_) => return false, // Fail closed
510        };
511
512        let transaction = match sync_database.new_transaction().await {
513            Ok(tx) => tx,
514            Err(_) => return false, // Fail closed
515        };
516
517        // Use UserSyncManager to get combined settings
518        let user_mgr = UserSyncManager::new(&transaction);
519        match user_mgr.get_combined_settings(tree_id).await {
520            Ok(Some(settings)) => settings.sync_enabled,
521            _ => false, // Fail closed: no settings or error
522        }
523    }
524
525    /// Register an incoming peer and add their addresses to the peer list.
526    ///
527    /// This method registers a peer that initiated a connection to us during handshake.
528    /// It adds both the peer-advertised addresses and the transport-provided remote address.
529    ///
530    /// # Arguments
531    /// * `peer_pubkey` - The peer's public key
532    /// * `display_name` - Optional display name for the peer
533    /// * `advertised_addresses` - Addresses the peer advertised in their handshake
534    /// * `remote_address` - The actual address from which the connection originated
535    ///
536    /// # Returns
537    /// Result indicating success or failure of registration
538    async fn register_incoming_peer(
539        &self,
540        peer_pubkey: &PublicKey,
541        display_name: Option<&str>,
542        advertised_addresses: &[Address],
543        remote_address: &Option<Address>,
544    ) -> Result<()> {
545        let sync_tree = self.get_sync_tree().await?;
546        let txn = sync_tree.new_transaction().await?;
547        let peer_manager = PeerManager::new(&txn);
548
549        // Try to register the peer (ignore if already exists)
550        match peer_manager.register_peer(peer_pubkey, display_name).await {
551            Ok(()) => {
552                info!(peer_pubkey = %peer_pubkey, "Registered new incoming peer");
553            }
554            Err(Error::Sync(ref e)) if matches!(**e, SyncError::PeerAlreadyExists(_)) => {
555                debug!(peer_pubkey = %peer_pubkey, "Peer already registered, updating addresses");
556            }
557            Err(e) => return Err(e),
558        }
559
560        // Add all advertised addresses
561        for addr in advertised_addresses {
562            if let Err(e) = peer_manager.add_address(peer_pubkey, addr.clone()).await {
563                warn!(peer_pubkey = %peer_pubkey, address = ?addr, error = %e, "Failed to add advertised address");
564            }
565        }
566
567        // Add the remote address from transport if available
568        if let Some(addr) = remote_address
569            && let Err(e) = peer_manager.add_address(peer_pubkey, addr.clone()).await
570        {
571            warn!(peer_pubkey = %peer_pubkey, address = ?addr, error = %e, "Failed to add remote address");
572        }
573
574        txn.commit().await?;
575        Ok(())
576    }
577
578    /// Track tree/peer sync relationship when a peer requests a tree.
579    ///
580    /// This method adds the tree to the peer's sync list, enabling bidirectional
581    /// sync for the requested tree. This is critical for `sync_on_commit` to work
582    /// in both directions.
583    ///
584    /// # Arguments
585    /// * `tree_id` - The ID of the tree being requested
586    /// * `peer_pubkey` - The public key of the peer requesting the tree (device key, not auth key)
587    ///
588    /// # Returns
589    /// Result indicating success or failure
590    async fn track_tree_sync_relationship(
591        &self,
592        tree_id: &ID,
593        peer_pubkey: &PublicKey,
594    ) -> Result<()> {
595        let sync_tree = self.get_sync_tree().await?;
596        let txn = sync_tree.new_transaction().await?;
597        let peer_manager = PeerManager::new(&txn);
598
599        // Add the tree sync relationship
600        peer_manager.add_tree_sync(peer_pubkey, tree_id).await?;
601        txn.commit().await?;
602
603        debug!(tree_id = %tree_id, peer_pubkey = %peer_pubkey, "Tracked tree/peer sync relationship");
604        Ok(())
605    }
606
607    /// Handle a handshake request from a peer.
608    async fn handle_handshake(
609        &self,
610        request: &HandshakeRequest,
611        context: &RequestContext,
612    ) -> SyncResponse {
613        async move {
614            debug!(
615                peer_device_id = %request.device_id,
616                peer_public_key = %request.public_key,
617                display_name = ?request.display_name,
618                protocol_version = request.protocol_version,
619                "Processing handshake request"
620            );
621
622            // Check protocol version compatibility
623            if request.protocol_version != PROTOCOL_VERSION {
624                warn!(
625                    expected = PROTOCOL_VERSION,
626                    received = request.protocol_version,
627                    "Protocol version mismatch"
628                );
629                return SyncResponse::Error(format!(
630                    "Protocol version mismatch: expected {}, got {}",
631                    PROTOCOL_VERSION, request.protocol_version
632                ));
633            }
634
635            // Get device signing key from backend
636            let instance = match self.instance() {
637                Ok(i) => i,
638                Err(e) => {
639                    error!(error = %e, "Failed to get instance");
640                    return SyncResponse::Error(format!("Failed to get instance: {e}"));
641                }
642            };
643            let signing_key = match instance.signing_key() {
644                Ok(k) => k.clone(),
645                Err(e) => {
646                    error!(error = %e, "Failed to get device key");
647                    return SyncResponse::Error(format!("Failed to get device key: {e}"));
648                }
649            };
650
651            // Generate device ID and public key from signing key
652            let public_key = signing_key.public_key();
653            let device_id = public_key.clone(); // Device ID is the public key
654
655            // Sign the challenge with our device key to prove identity
656            let challenge_response = create_challenge_response(&request.challenge, &signing_key);
657
658            // Generate a new challenge for mutual authentication
659            let new_challenge = generate_challenge();
660
661            // Get available trees for discovery
662            let available_trees = self.get_available_trees().await;
663
664            // Register the peer and add their addresses to our peer list
665            match self.register_incoming_peer(&request.public_key, request.display_name.as_deref(), &request.listen_addresses, &context.remote_address).await {
666                Ok(()) => {
667                    debug!(peer_pubkey = %request.public_key, "Successfully registered incoming peer");
668                }
669                Err(e) => {
670                    // Log the error but don't fail the handshake - peer registration is best-effort
671                    warn!(peer_pubkey = %request.public_key, error = %e, "Failed to register incoming peer");
672                }
673            }
674
675            info!(
676                our_device_id = %device_id,
677                peer_device_id = %request.device_id,
678                tree_count = available_trees.len(),
679                "Handshake completed successfully"
680            );
681
682            SyncResponse::Handshake(HandshakeResponse {
683                device_id,
684                public_key,
685                display_name: Some("Eidetica Peer".to_string()),
686                protocol_version: PROTOCOL_VERSION,
687                challenge_response,
688                new_challenge,
689                available_trees,
690            })
691        }
692        .instrument(info_span!("handle_handshake", peer = %request.device_id))
693        .await
694    }
695
696    /// Handle a unified sync tree request (bootstrap or incremental).
697    ///
698    /// This method routes between two sync modes:
699    /// 1. **Bootstrap**: When peer has no tips (empty database), sends complete tree
700    /// 2. **Incremental**: When peer has existing tips, sends only new entries
701    ///
702    /// # Bootstrap Authentication
703    /// During bootstrap, if the peer provides authentication credentials:
704    /// - `requesting_key`: Public key to add
705    /// - `requesting_key_name`: Name for the key
706    /// - `requested_permission`: Access level requested
707    ///
708    /// The handler will evaluate the bootstrap policy and either:
709    /// - Auto-approve and add the key immediately
710    /// - Store request for manual approval
711    /// - Proceed without authentication (anonymous bootstrap)
712    async fn handle_sync_tree(
713        &self,
714        request: &SyncTreeRequest,
715        context: &RequestContext,
716    ) -> SyncResponse {
717        async move {
718            trace!(tree_id = %request.tree_id, "Processing sync tree request");
719
720            // Track tree/peer sync relationship for bidirectional sync
721            // IMPORTANT: Only use context.peer_pubkey (device key from handshake)
722            // Do NOT use request.requesting_key (that's an auth key for database access)
723            if let Some(peer_pubkey) = &context.peer_pubkey {
724                if let Err(e) = self.track_tree_sync_relationship(&request.tree_id, peer_pubkey).await {
725                    // Log the error but don't fail the sync - relationship tracking is best-effort
726                    warn!(tree_id = %request.tree_id, peer_pubkey = %peer_pubkey, error = %e, "Failed to track tree/peer relationship");
727                }
728            } else {
729                debug!(tree_id = %request.tree_id, "No peer pubkey in context, skipping relationship tracking");
730            }
731
732            // Check if peer needs bootstrap (empty tips indicates no local data)
733            if request.our_tips.is_empty() {
734                debug!(tree_id = %request.tree_id, "Peer needs bootstrap - sending full tree");
735                return self.handle_bootstrap_request(request).await;
736            }
737
738            // Handle incremental sync (peer has existing data, needs updates)
739            debug!(tree_id = %request.tree_id, peer_tips = request.our_tips.len(), "Handling incremental sync");
740            self.handle_incremental_sync(request).await
741        }
742        .instrument(info_span!("handle_sync_tree", tree = %request.tree_id))
743        .await
744    }
745
746    /// Handle bootstrap request by sending complete tree state and optionally approving auth key.
747    ///
748    /// Bootstrap is the initial synchronization when a peer has no local data for a tree.
749    /// This method:
750    /// 1. Validates the tree exists and sync is enabled
751    /// 2. Processes authentication and permission resolution
752    /// 3. Sends all entries from the tree to the peer
753    ///
754    /// # Authentication Flow
755    ///
756    /// The bootstrap process handles three authentication scenarios:
757    ///
758    /// ## 1. Explicit Permission Request
759    /// When all three auth parameters are provided (`requesting_key`, `requesting_key_name`, `requested_permission`):
760    /// - Check if key already has sufficient permissions
761    /// - If yes: Approve immediately without adding key
762    /// - If no: Store request for manual approval and return `BootstrapPending`
763    ///
764    /// ## 2. Auto-Detection
765    /// When key is provided but `requested_permission` is `None`:
766    /// - Look up key's existing permissions in database auth settings
767    /// - Uses `find_all_sigkeys_for_pubkey()` to find all permissions (direct + global wildcard)
768    /// - If key found: Use highest available permission and approve immediately
769    /// - If key not found: Reject with authentication error
770    ///
771    /// ## 3. Unauthenticated Access
772    /// When no `requesting_key` is provided:
773    /// - Only allowed if database has no auth configured or has global wildcard permission
774    /// - Otherwise rejected with authentication required error
775    ///
776    /// # Note on Key Verification
777    ///
778    /// This function does not verify that the peer actually controls the `requesting_key`.
779    /// The `requesting_key` parameter is an unverified string from the client.
780    ///
781    /// **This is not a security vulnerability** because:
782    /// - Approval only adds the public key to database auth settings
783    /// - Actual database access requires signing entries with the corresponding private key
784    /// - If an attacker claims someone else's public key, approval grants access to the
785    ///   legitimate key holder (who has the private key), not the attacker
786    ///
787    /// The lack of verification may cause:
788    /// - Audit trail confusion (request appears to come from a different identity)
789    /// - Admins approving access for keys that didn't actually request it
790    ///
791    /// # Arguments
792    /// * `tree_id` - The database/tree to bootstrap
793    /// * `requesting_key` - Optional public key requesting access (unverified, but safe - see above)
794    /// * `requesting_key_name` - Optional name/identifier for the key (unverified)
795    /// * `requested_permission` - Optional permission level requested (if None, auto-detects from auth settings)
796    ///
797    /// # Returns
798    /// - `BootstrapResponse`: Contains entries and approval status (key_approved, granted_permission)
799    /// - `BootstrapPending`: Manual approval required (request queued)
800    /// - `Error`: Tree not found, auth required, key not authorized, or processing failure
801    async fn handle_bootstrap_request(&self, request: &SyncTreeRequest) -> SyncResponse {
802        let tree_id = &request.tree_id;
803        let requesting_key = request.requesting_key.as_ref();
804        let requesting_key_name = request.requesting_key_name.as_deref();
805        let requested_permission = request.requested_permission;
806        let metadata = request.metadata.clone();
807
808        // SECURITY: Check if database has sync enabled (FIRST CHECK - before anything else)
809        // This prevents information leakage about database existence: the gate
810        // returns false both for databases that are absent and for databases that
811        // are present-but-not-tracked-for-sync, and we deliberately respond with
812        // the same opaque "Tree not found" to peers in either case.
813        if !self.is_database_sync_enabled(tree_id).await {
814            warn!(
815                tree_id = %tree_id,
816                requesting_key = ?requesting_key,
817                requesting_key_name = ?requesting_key_name,
818                "Bootstrap request rejected: database is absent or has no user with sync enabled (responding as not-found)"
819            );
820            return SyncResponse::Error(format!("Tree not found: {tree_id}"));
821        }
822
823        // Get the root entry (to verify tree exists)
824        let instance = match self.instance() {
825            Ok(i) => i,
826            Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
827        };
828        let _root_entry = match instance.backend().get(tree_id).await {
829            Ok(entry) => entry,
830            Err(e) if e.is_not_found() => {
831                warn!(
832                    tree_id = %tree_id,
833                    requesting_key = ?requesting_key,
834                    requesting_key_name = ?requesting_key_name,
835                    "Bootstrap request rejected: a user has this tree marked sync-enabled but the backend has no root entry for it"
836                );
837                return SyncResponse::Error(format!("Tree not found: {tree_id}"));
838            }
839            Err(e) => {
840                error!(tree_id = %tree_id, error = %e, "Failed to get root entry");
841                return SyncResponse::Error(format!("Failed to get tree root: {e}"));
842            }
843        };
844
845        // Check if database has authentication configured
846        let auth_configured = match self.check_if_database_has_auth(tree_id).await {
847            Ok(has_auth) => has_auth,
848            Err(e) => {
849                error!(tree_id = %tree_id, error = %e, "Failed to check if database has auth");
850                return SyncResponse::Error(format!("Failed to check database auth: {e}"));
851            }
852        };
853
854        // If auth is configured but no credentials provided, reject the request
855        if auth_configured && requesting_key.is_none() {
856            warn!(
857                tree_id = %tree_id,
858                "Unauthenticated bootstrap request rejected - database requires authentication"
859            );
860            return SyncResponse::Error(
861                "Authentication required: This database requires authenticated access. \
862                 Please provide credentials (requesting_key, requesting_key_name, requested_permission) \
863                 to bootstrap sync.".to_string()
864            );
865        }
866
867        // Handle key approval for bootstrap requests FIRST
868        let (key_approved, granted_permission) = match (
869            requesting_key,
870            requesting_key_name,
871            requested_permission,
872        ) {
873            // Case 1: All three parameters provided - explicit permission request
874            (Some(key), Some(key_name), Some(permission)) => {
875                info!(
876                    tree_id = %tree_id,
877                    requesting_key = %key,
878                    key_name = %key_name,
879                    requested_permission = ?permission,
880                    "Processing key approval request for bootstrap"
881                );
882
883                // Check if the requesting key already has sufficient permissions through existing auth
884                match self
885                    .check_proven_auth_permission(request, key, &permission, auth_configured)
886                    .await
887                {
888                    Ok(true) => {
889                        // Key already has sufficient permission - approve without adding
890                        info!(
891                            tree_id = %tree_id,
892                            key = %key,
893                            permission = ?permission,
894                            "Bootstrap approved via existing auth permission - no key added"
895                        );
896                        (true, Some(permission))
897                    }
898                    Ok(false) => {
899                        // No existing permission, store request for manual approval
900                        info!(tree_id = %tree_id, "Bootstrap key approval requested - storing for manual approval");
901
902                        // Store the bootstrap request in sync database for manual approval
903                        match self
904                            .store_bootstrap_request(tree_id, key, key_name, &permission, metadata)
905                            .await
906                        {
907                            Ok(request_id) => {
908                                info!(
909                                    tree_id = %tree_id,
910                                    request_id = %request_id,
911                                    "Bootstrap request stored for manual approval"
912                                );
913                                return SyncResponse::BootstrapPending {
914                                    request_id,
915                                    message: "Bootstrap request pending manual approval"
916                                        .to_string(),
917                                };
918                            }
919                            Err(e) => {
920                                error!(
921                                    tree_id = %tree_id,
922                                    error = %e,
923                                    "Failed to store bootstrap request"
924                                );
925                                return SyncResponse::Error(format!(
926                                    "Failed to store bootstrap request: {e}"
927                                ));
928                            }
929                        }
930                    }
931                    Err(e) => {
932                        error!(tree_id = %tree_id, error = %e, "Failed to check global permission for bootstrap");
933                        return SyncResponse::Error(format!("Global permission check failed: {e}"));
934                    }
935                }
936            }
937
938            // Case 2: Key provided but permission not specified - auto-detect from auth settings
939            (Some(key), Some(_key_name), None) => {
940                info!(
941                    tree_id = %tree_id,
942                    requesting_key = %key,
943                    "Auto-detecting permission from auth settings for bootstrap request"
944                );
945
946                if let Err(e) = self.prove_possession_if_required(request, key, auth_configured) {
947                    warn!(
948                        tree_id = %tree_id,
949                        requesting_key = %key,
950                        error = %e,
951                        "Bootstrap request rejected: caller did not prove it holds the key it claims"
952                    );
953                    return SyncResponse::Error(e.to_string());
954                }
955
956                match self.get_key_highest_permission(tree_id, key).await {
957                    Ok(Some(permission)) => {
958                        info!(
959                            tree_id = %tree_id,
960                            requesting_key = %key,
961                            detected_permission = ?permission,
962                            "Approved bootstrap using auto-detected permission from auth settings"
963                        );
964                        (true, Some(permission))
965                    }
966                    Ok(None) => {
967                        warn!(
968                            tree_id = %tree_id,
969                            requesting_key = %key,
970                            "Key not found in auth settings - rejecting bootstrap request"
971                        );
972                        return SyncResponse::Error(
973                            "Authentication required: provided key is not authorized for this database".to_string()
974                        );
975                    }
976                    Err(e) => {
977                        error!(
978                            tree_id = %tree_id,
979                            requesting_key = %key,
980                            error = %e,
981                            "Failed to lookup key permissions"
982                        );
983                        return SyncResponse::Error(format!("Failed to access auth settings: {e}"));
984                    }
985                }
986            }
987
988            // Case 3: No key provided, or key provided without key_name - unauthenticated access
989            _ => {
990                debug!(
991                    tree_id = %tree_id,
992                    "No authentication credentials provided - proceeding with unauthenticated bootstrap"
993                );
994                (false, None)
995            }
996        };
997
998        // A database with auth configured serves entries only to a caller that
999        // proved it holds a key with read access. Cases that fall through
1000        // without approval (no credentials, or a key with no key name) must not
1001        // be served just because they reached this point.
1002        if auth_configured && !key_approved {
1003            warn!(
1004                tree_id = %tree_id,
1005                requesting_key = ?requesting_key,
1006                "Bootstrap request rejected: no proven authority for a database that requires authentication"
1007            );
1008            return SyncResponse::Error(
1009                SyncError::AuthenticationRequired(tree_id.to_string()).to_string(),
1010            );
1011        }
1012
1013        // NOW collect all entries after key approval (so we get the updated database state)
1014        let all_entries = match self.collect_all_entries_for_bootstrap(tree_id).await {
1015            Ok(entries) => entries,
1016            Err(e) => {
1017                error!(tree_id = %tree_id, error = %e, "Failed to collect all entries for bootstrap after key approval");
1018                return SyncResponse::Error(format!(
1019                    "Failed to collect all entries for bootstrap: {e}"
1020                ));
1021            }
1022        };
1023
1024        // For bootstrap, we need to send the actual root entry (tree_id) as root_entry
1025        // The root_entry should always be the tree's root, not a tip
1026        let instance = match self.instance() {
1027            Ok(i) => i,
1028            Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
1029        };
1030        let root_entry = match instance.backend().get(tree_id).await {
1031            Ok(entry) => entry,
1032            Err(e) => {
1033                error!(tree_id = %tree_id, error = %e, "Failed to get root entry");
1034                return SyncResponse::Error(format!("Failed to get root entry: {e}"));
1035            }
1036        };
1037
1038        // Filter out the root from all_entries since we send it separately as root_entry
1039        let other_entries: Vec<_> = all_entries
1040            .into_iter()
1041            .filter(|entry| entry.id() != *tree_id)
1042            .collect();
1043
1044        info!(
1045            tree_id = %tree_id,
1046            entry_count = other_entries.len() + 1,
1047            key_approved = key_approved,
1048            "Sending bootstrap response"
1049        );
1050
1051        SyncResponse::Bootstrap(BootstrapResponse {
1052            tree_id: tree_id.clone(),
1053            root_entry,
1054            all_entries: other_entries,
1055            key_approved,
1056            granted_permission,
1057        })
1058    }
1059
1060    /// Handle incremental sync request.
1061    ///
1062    /// The caller selects this path by sending any non-empty tip list, so it
1063    /// enforces the same read policy bootstrap does. Without that, a single
1064    /// fabricated tip — which matches nothing in our DAG and therefore never
1065    /// stops the ancestor walk — returns the entire tree.
1066    async fn handle_incremental_sync(&self, request: &SyncTreeRequest) -> SyncResponse {
1067        let tree_id = &request.tree_id;
1068        let peer_tips = request.our_tips.tips();
1069
1070        // SECURITY: Check if database has sync enabled (FIRST CHECK - before anything else)
1071        // This prevents information leakage about database existence: the gate
1072        // returns false both for databases that are absent and for databases that
1073        // are present-but-not-tracked-for-sync, and we deliberately respond with
1074        // the same opaque "Tree not found" to peers in either case.
1075        if !self.is_database_sync_enabled(tree_id).await {
1076            warn!(
1077                tree_id = %tree_id,
1078                peer_tip_count = peer_tips.len(),
1079                "Incremental sync request rejected: database is absent or has no user with sync enabled (responding as not-found)"
1080            );
1081            return SyncResponse::Error(format!("Tree not found: {tree_id}"));
1082        }
1083
1084        if let Err(e) = self.authorize_read(request).await {
1085            warn!(
1086                tree_id = %tree_id,
1087                peer_tip_count = peer_tips.len(),
1088                error = %e,
1089                "Incremental sync request rejected: caller is not authorized to read this database"
1090            );
1091            return SyncResponse::Error(e.to_string());
1092        }
1093
1094        // Get our current tips
1095        let instance = match self.instance() {
1096            Ok(i) => i,
1097            Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
1098        };
1099        let our_tips: Vec<ID> = match instance.backend().snapshot(tree_id).await {
1100            Ok(snap) => snap.into_tips(),
1101            Err(e) => {
1102                error!(tree_id = %tree_id, error = %e, "Failed to get our tips");
1103                return SyncResponse::Error(format!("Failed to get tips: {e}"));
1104            }
1105        };
1106
1107        // Find entries peer is missing
1108        let missing_entries = match self
1109            .find_missing_entries_for_peer(&our_tips, peer_tips)
1110            .await
1111        {
1112            Ok(entries) => entries,
1113            Err(e) => {
1114                error!(tree_id = %tree_id, error = %e, "Failed to find missing entries");
1115                return SyncResponse::Error(format!("Failed to find missing entries: {e}"));
1116            }
1117        };
1118
1119        debug!(
1120            tree_id = %tree_id,
1121            our_tips = our_tips.len(),
1122            peer_tips = peer_tips.len(),
1123            missing_count = missing_entries.len(),
1124            "Sending incremental sync response"
1125        );
1126
1127        SyncResponse::Incremental(IncrementalResponse {
1128            tree_id: tree_id.clone(),
1129            their_tips: our_tips,
1130            missing_entries,
1131        })
1132    }
1133}