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}