1
// org.freedesktop.Secret.Service
2

            
3
use std::{
4
    collections::HashMap,
5
    sync::{Arc, OnceLock},
6
};
7

            
8
use oo7::{
9
    Key, Secret,
10
    dbus::{
11
        Algorithm, ServiceError,
12
        api::{DBusSecretInner, Properties},
13
    },
14
    file::{Keyring, LockedKeyring, UnlockedKeyring},
15
};
16
use tokio::sync::{Mutex, RwLock};
17
use tokio_stream::StreamExt;
18
use zbus::{
19
    names::UniqueName,
20
    object_server::SignalEmitter,
21
    proxy::Defaults,
22
    zvariant::{ObjectPath, Optional, OwnedObjectPath, OwnedValue, Value},
23
};
24

            
25
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
26
pub use crate::gnome::internal::InternalInterface;
27
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
28
use crate::plasma::prompter::in_plasma_environment;
29
use crate::{
30
    collection::Collection,
31
    error::{Error, custom_service_error},
32
    migration::{self, PendingMigration},
33
    prompt::{Prompt, PromptAction, PromptRole},
34
    session::Session,
35
};
36

            
37
const DEFAULT_COLLECTION_ALIAS_PATH: ObjectPath<'static> =
38
    ObjectPath::from_static_str_unchecked("/org/freedesktop/secrets/aliases/default");
39

            
40
/// Prompter type
41
#[derive(Clone, Copy, PartialEq, Eq)]
42
pub enum PrompterType {
43
    #[allow(clippy::upper_case_acronyms)]
44
    GNOME,
45
    Plasma,
46
    Cli,
47
}
48

            
49
#[derive(Clone)]
50
pub struct Service {
51
    // Properties
52
    pub(crate) collections: Arc<Mutex<HashMap<OwnedObjectPath, Collection>>>,
53
    // Other attributes
54
    connection: Arc<OnceLock<zbus::Connection>>,
55
    // sessions mapped to their corresponding object path on the bus
56
    sessions: Arc<Mutex<HashMap<OwnedObjectPath, Session>>>,
57
    session_index: Arc<RwLock<u32>>,
58
    // prompts mapped to their corresponding object path on the bus
59
    prompts: Arc<Mutex<HashMap<OwnedObjectPath, Prompt>>>,
60
    prompt_index: Arc<RwLock<u32>>,
61
    // pending collection creations: prompt_path -> (label, alias)
62
    pending_collections: Arc<Mutex<HashMap<OwnedObjectPath, (String, String)>>>,
63
    // pending keyring migrations: name -> migration
64
    pub(crate) pending_migrations: Arc<Mutex<HashMap<String, PendingMigration>>>,
65
    // Data directory for keyrings (e.g., ~/.local/share or test temp dir)
66
    data_dir: std::path::PathBuf,
67
    // PAM socket path (None for tests that don't need PAM listener)
68
    pub(crate) pam_socket: Option<std::path::PathBuf>,
69
    // Override for prompter type (mainly for tests)
70
    pub(crate) prompter_type_override: Arc<Mutex<Option<PrompterType>>>,
71
}
72

            
73
#[zbus::interface(name = "org.freedesktop.Secret.Service")]
74
impl Service {
75
    #[zbus(out_args("output", "result"))]
76
36
    pub async fn open_session(
77
        &self,
78
        algorithm: Algorithm,
79
        input: Value<'_>,
80
        #[zbus(header)] header: zbus::message::Header<'_>,
81
        #[zbus(object_server)] object_server: &zbus::ObjectServer,
82
    ) -> Result<(OwnedValue, OwnedObjectPath), ServiceError> {
83
68
        let (public_key, aes_key) = match algorithm {
84
35
            Algorithm::Plain => (None, None),
85
            Algorithm::Encrypted => {
86
35
                let client_public_key = Key::try_from(input).map_err(|err| {
87
                    custom_service_error(&format!(
88
                        "Input Value could not be converted into a Key {err}."
89
                    ))
90
                })?;
91
32
                let private_key = Key::generate_private_key().map_err(|err| {
92
                    custom_service_error(&format!("Failed to generate private key {err}."))
93
                })?;
94
                (
95
31
                    Some(Key::generate_public_key(&private_key).map_err(|err| {
96
                        custom_service_error(&format!("Failed to generate public key {err}."))
97
                    })?),
98
15
                    Some(
99
32
                        Key::generate_aes_key(&private_key, &client_public_key).map_err(|err| {
100
                            custom_service_error(&format!("Failed to generate aes key {err}."))
101
                        })?,
102
                    ),
103
                )
104
            }
105
        };
106

            
107
70
        let sender = if let Some(s) = header.sender() {
108
            s.to_owned()
109
        } else {
110
            #[cfg(any(test, feature = "test-util"))]
111
            {
112
                // For p2p test connections, use a dummy sender since p2p connections
113
                // don't have a bus to assign unique names
114
                UniqueName::try_from(":p2p.test").unwrap()
115
            }
116
            #[cfg(not(any(test, feature = "test-util")))]
117
            {
118
                return Err(custom_service_error("Failed to get sender from header."));
119
            }
120
        };
121

            
122
193
        let peer_name = async {
123
64
            let proxy = zbus::fdo::DBusProxy::new(self.connection()).await.ok()?;
124
154
            let pid = proxy
125
60
                .get_connection_unix_process_id(sender.as_ref().into())
126
115
                .await
127
                .ok()?;
128
            let cmdline = tokio::fs::read(format!("/proc/{pid}/cmdline")).await.ok()?;
129
            let name = cmdline.split(|&b| b == 0).next()?;
130
            let name = std::path::Path::new(std::str::from_utf8(name).ok()?)
131
                .file_name()?
132
                .to_str()?;
133
            Some(format!("{name}[{pid}]"))
134
        }
135
134
        .await;
136
        let session = Session::new(
137
64
            aes_key.map(Arc::new),
138
64
            self.clone(),
139
32
            sender.clone(),
140
32
            peer_name,
141
        )
142
92
        .await;
143
53
        let path = OwnedObjectPath::from(session.path().clone());
144

            
145
48
        match session.peer_name() {
146
            Some(name) => {
147
                tracing::info!("Client {} ({}) connected, session: {}", sender, name, path)
148
            }
149
45
            None => tracing::info!("Client {} connected, session: {}", sender, path),
150
        }
151

            
152
98
        self.sessions
153
            .lock()
154
85
            .await
155
57
            .insert(path.clone(), session.clone());
156

            
157
22
        object_server.at(&path, session).await?;
158

            
159
30
        let service_key = public_key
160
21
            .map(OwnedValue::from)
161
85
            .unwrap_or_else(|| Value::new::<Vec<u8>>(vec![]).try_into_owned().unwrap());
162

            
163
24
        Ok((service_key, path))
164
    }
165

            
166
    #[zbus(out_args("collection", "prompt"))]
167
11
    pub async fn create_collection(
168
        &self,
169
        properties: Properties,
170
        alias: &str,
171
    ) -> Result<(OwnedObjectPath, ObjectPath<'_>), ServiceError> {
172
21
        let label = properties.label().to_owned();
173
21
        let alias = alias.to_owned();
174

            
175
        // Create a prompt to get the password for the new collection
176
        let prompt = Prompt::new(
177
21
            self.clone(),
178
            PromptRole::CreateCollection,
179
12
            label.clone(),
180
12
            None,
181
        )
182
35
        .await;
183
21
        let prompt_path = OwnedObjectPath::from(prompt.path().clone());
184

            
185
        // Store the collection metadata for later creation
186
40
        self.pending_collections
187
            .lock()
188
33
            .await
189
11
            .insert(prompt_path.clone(), (label, alias));
190

            
191
        // Create the collection creation action
192
21
        let service = self.clone();
193
10
        let creation_prompt_path = prompt_path.clone();
194
49
        let action = PromptAction::new(move |secret: Option<Secret>| async move {
195
36
            let collection_path = service
196
19
                .complete_collection_creation(&creation_prompt_path, secret)
197
41
                .await?;
198

            
199
20
            Ok(Value::new(collection_path).try_into_owned().unwrap())
200
        });
201

            
202
11
        prompt.set_action(action).await;
203

            
204
        // Register the prompt
205
55
        self.prompts
206
            .lock()
207
35
            .await
208
24
            .insert(prompt_path.clone(), prompt.clone());
209

            
210
10
        self.object_server().at(&prompt_path, prompt).await?;
211

            
212
16
        tracing::debug!("CreateCollection prompt created at `{}`", prompt_path);
213

            
214
        // Return empty collection path and the prompt path
215
21
        Ok((OwnedObjectPath::default(), prompt_path.into()))
216
    }
217

            
218
    #[zbus(out_args("unlocked", "locked"))]
219
4
    pub async fn search_items(
220
        &self,
221
        attributes: HashMap<String, String>,
222
    ) -> Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>), ServiceError> {
223
4
        let mut unlocked = Vec::new();
224
4
        let mut locked = Vec::new();
225
8
        let collections = self.collections.lock().await;
226

            
227
12
        for (_path, collection) in collections.iter() {
228
16
            let items = collection.search_inner_items(&attributes).await?;
229
8
            for item in items {
230
19
                if item.is_locked().await {
231
8
                    locked.push(item.path().clone().into());
232
                } else {
233
10
                    unlocked.push(item.path().clone().into());
234
                }
235
            }
236
        }
237

            
238
8
        if unlocked.is_empty() && locked.is_empty() {
239
8
            tracing::debug!(
240
                "Items with attributes {:?} does not exist in any collection.",
241
                attributes
242
            );
243
        } else {
244
8
            tracing::debug!("Items with attributes {:?} found.", attributes);
245
        }
246

            
247
4
        Ok((unlocked, locked))
248
    }
249

            
250
    #[zbus(out_args("unlocked", "prompt"))]
251
10
    pub async fn unlock(
252
        &self,
253
        objects: Vec<OwnedObjectPath>,
254
    ) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
255
23
        tracing::info!("Unlock requested for {} objects.", objects.len());
256
20
        let (unlocked, not_unlocked) = self.set_locked(false, &objects).await?;
257
20
        if !not_unlocked.is_empty() {
258
            // Extract the label and collection before creating the prompt
259
16
            let label = self.extract_label_from_objects(&not_unlocked).await;
260
16
            let collection = self.extract_collection_from_objects(&not_unlocked).await;
261

            
262
16
            let prompt = Prompt::new(self.clone(), PromptRole::Unlock, label, collection).await;
263
16
            let path = OwnedObjectPath::from(prompt.path().clone());
264

            
265
            // Create the unlock action
266
8
            let service = self.clone();
267
57
            let action = PromptAction::new(move |secret: Option<Secret>| async move {
268
                // The prompter will handle secret validation
269
                // Here we just perform the unlock operation
270

            
271
                // First, check for pending migrations (without holding collections lock)
272
32
                for object in &not_unlocked {
273
                    let collection = {
274
24
                        let collections = service.collections.lock().await;
275
16
                        collections.get(object).cloned()
276
                    };
277

            
278
8
                    if let Some(collection) = collection {
279
                        // Check if this collection has a pending migration by name
280
                        let migration_opt = {
281
24
                            let pending = service.pending_migrations.lock().await;
282
16
                            pending.get(collection.name()).cloned()
283
                        };
284

            
285
8
                        if let Some(migration) = migration_opt {
286
9
                            let migration_name = migration.name();
287
4
                            tracing::debug!(
288
                                "Attempting migration for '{}' during unlock",
289
                                migration_name
290
                            );
291

            
292
                            // Attempt migration with the provided secret (no locks held)
293
16
                            match migration.migrate(&service.data_dir, secret.as_ref()).await {
294
4
                                Ok(unlocked_keyring) => {
295
9
                                    tracing::info!(
296
                                        "Successfully migrated '{}' during unlock",
297
                                        migration_name
298
                                    );
299

            
300
                                    // Replace the keyring in the collection
301
12
                                    let mut keyring_guard = collection.keyring.write().await;
302
4
                                    *keyring_guard = Some(Keyring::Unlocked(unlocked_keyring));
303
4
                                    drop(keyring_guard);
304

            
305
                                    // Dispatch items from the migrated keyring
306
13
                                    if let Err(e) = collection.dispatch_items().await {
307
                                        tracing::error!(
308
                                            "Failed to dispatch items after migration: {}",
309
                                            e
310
                                        );
311
                                    }
312

            
313
                                    // Remove from pending migrations
314
20
                                    service
315
                                        .pending_migrations
316
5
                                        .lock()
317
20
                                        .await
318
5
                                        .remove(migration_name);
319
                                }
320
                                Err(e) => {
321
                                    tracing::warn!(
322
                                        "Failed to migrate '{}' during unlock: {}",
323
                                        migration_name,
324
                                        e
325
                                    );
326
                                    let _ = collection.set_locked(false, secret.clone()).await;
327
                                }
328
                            }
329
                        } else {
330
                            // Normal unlock
331
26
                            let _ = collection.set_locked(false, secret.clone()).await;
332
                        }
333
                    } else {
334
                        // Try to find as item within collections
335
12
                        let collections = service.collections.lock().await;
336
4
                        let mut found_collection = None;
337
8
                        for (_path, collection) in collections.iter() {
338
12
                            if let Some(item) = collection.item_from_path(object).await {
339
4
                                found_collection = Some((
340
4
                                    collection.clone(),
341
4
                                    item.clone(),
342
12
                                    collection.is_locked().await,
343
                                ));
344
                                break;
345
                            }
346
                        }
347
4
                        drop(collections);
348

            
349
4
                        if let Some((collection, item, is_locked)) = found_collection {
350
4
                            if is_locked {
351
12
                                let _ = collection.set_locked(false, secret.clone()).await;
352
                            } else {
353
                                let keyring = collection.keyring.read().await;
354
                                match keyring.as_ref() {
355
                                    Some(k) if !k.is_locked() => {
356
                                        let _ = item.set_locked(false, k.as_unlocked()).await;
357
                                    }
358
                                    _ => {
359
                                        drop(keyring);
360
                                        let _ = collection.set_locked(false, secret.clone()).await;
361
                                    }
362
                                }
363
                            }
364
                        }
365
                    }
366
                }
367
9
                Ok(Value::new(not_unlocked).try_into_owned().unwrap())
368
            });
369

            
370
8
            prompt.set_action(action).await;
371

            
372
40
            self.prompts
373
                .lock()
374
24
                .await
375
16
                .insert(path.clone(), prompt.clone());
376

            
377
8
            self.object_server().at(&path, prompt).await?;
378
8
            return Ok((unlocked, path));
379
        }
380

            
381
8
        Ok((unlocked, OwnedObjectPath::default()))
382
    }
383

            
384
    #[zbus(out_args("locked", "Prompt"))]
385
10
    pub async fn lock(
386
        &self,
387
        objects: Vec<OwnedObjectPath>,
388
    ) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
389
23
        tracing::info!("Lock requested for {} objects.", objects.len());
390
22
        let (locked, not_locked) = self.set_locked(true, &objects).await?;
391
        // Locking never requires prompts, so not_locked should always be empty
392
        debug_assert!(
393
            not_locked.is_empty(),
394
            "Lock operation should never require prompts"
395
        );
396
10
        Ok((locked, OwnedObjectPath::default()))
397
    }
398

            
399
    #[zbus(out_args("secrets"))]
400
5
    pub async fn get_secrets(
401
        &self,
402
        items: Vec<OwnedObjectPath>,
403
        session: OwnedObjectPath,
404
    ) -> Result<HashMap<OwnedObjectPath, DBusSecretInner>, ServiceError> {
405
3
        tracing::debug!(
406
            "GetSecrets called for {} items with session {}.",
407
            items.len(),
408
            session
409
        );
410
4
        let mut secrets = HashMap::new();
411
8
        let collections = self.collections.lock().await;
412

            
413
16
        'outer: for (_path, collection) in collections.iter() {
414
16
            for item in &items {
415
12
                if let Some(item) = collection.item_from_path(item).await {
416
12
                    match item.get_secret(session.clone()).await {
417
4
                        Ok((secret,)) => {
418
8
                            secrets.insert(item.path().clone().into(), secret);
419
                            // To avoid iterating through all the remaining collections, if the
420
                            // items secrets are already retrieved.
421
4
                            if secrets.len() == items.len() {
422
                                break 'outer;
423
                            }
424
                        }
425
                        // Avoid erroring out if an item is locked.
426
                        Err(ServiceError::IsLocked(_)) => {
427
                            continue;
428
                        }
429
4
                        Err(err) => {
430
4
                            return Err(err);
431
                        }
432
                    };
433
                }
434
            }
435
        }
436

            
437
6
        tracing::debug!(
438
            "GetSecrets returned {} of {} requested secrets.",
439
            secrets.len(),
440
            items.len()
441
        );
442
4
        Ok(secrets)
443
    }
444

            
445
    #[zbus(out_args("collection"))]
446
84
    pub async fn read_alias(&self, name: &str) -> Result<OwnedObjectPath, ServiceError> {
447
        // Map "login" alias to "default" for compatibility with gnome-keyring
448
59
        let alias_to_find = if name == Self::LOGIN_ALIAS {
449
            oo7::dbus::Service::DEFAULT_COLLECTION
450
        } else {
451
23
            name
452
        };
453

            
454
23
        let collections = self.collections.lock().await;
455

            
456
82
        for (path, collection) in collections.iter() {
457
59
            if collection.alias().await == alias_to_find {
458
35
                tracing::debug!("Collection: {} found for alias: {}.", path, name);
459
41
                return Ok(path.to_owned());
460
            }
461
        }
462

            
463
8
        tracing::info!("Collection with alias {} does not exist.", name);
464

            
465
12
        Ok(OwnedObjectPath::default())
466
    }
467

            
468
4
    pub async fn set_alias(
469
        &self,
470
        name: &str,
471
        collection: OwnedObjectPath,
472
    ) -> Result<(), ServiceError> {
473
8
        let collections = self.collections.lock().await;
474

            
475
12
        for (path, other_collection) in collections.iter() {
476
8
            if *path == collection {
477
4
                other_collection.set_alias(name).await;
478

            
479
4
                tracing::info!("Collection: {} alias updated to {}.", collection, name);
480
4
                return Ok(());
481
            }
482
        }
483

            
484
4
        tracing::info!("Collection: {} does not exist.", collection);
485

            
486
8
        Err(ServiceError::NoSuchObject(format!(
487
            "The collection: {collection} does not exist.",
488
        )))
489
    }
490

            
491
    #[zbus(property, name = "Collections")]
492
106
    pub async fn collections(&self) -> Vec<OwnedObjectPath> {
493
56
        self.collections.lock().await.keys().cloned().collect()
494
    }
495

            
496
    #[zbus(signal, name = "CollectionCreated")]
497
    pub async fn collection_created(
498
9
        signal_emitter: &SignalEmitter<'_>,
499
10
        collection: &ObjectPath<'_>,
500
    ) -> zbus::Result<()>;
501

            
502
    #[zbus(signal, name = "CollectionDeleted")]
503
    pub async fn collection_deleted(
504
8
        signal_emitter: &SignalEmitter<'_>,
505
8
        collection: &ObjectPath<'_>,
506
    ) -> zbus::Result<()>;
507

            
508
    #[zbus(signal, name = "CollectionChanged")]
509
    pub async fn collection_changed(
510
10
        signal_emitter: &SignalEmitter<'_>,
511
10
        collection: &ObjectPath<'_>,
512
    ) -> zbus::Result<()>;
513
}
514

            
515
impl Service {
516
    const LOGIN_ALIAS: &str = "login";
517

            
518
    /// Set the prompter type override
519
    #[allow(unused)]
520
136
    pub(crate) async fn set_prompter_type(&self, prompter_type: PrompterType) {
521
68
        *self.prompter_type_override.lock().await = Some(prompter_type);
522
    }
523

            
524
    /// Get the prompter type to use
525
49
    pub(crate) async fn prompter_type(&self) -> PrompterType {
526
38
        if let Some(override_type) = self.prompter_type_override.lock().await.as_ref() {
527
12
            return *override_type;
528
        }
529

            
530
        let has_display = std::env::var_os("DISPLAY").is_some_and(|v| !v.is_empty())
531
            || std::env::var_os("WAYLAND_DISPLAY").is_some_and(|v| !v.is_empty());
532

            
533
        if has_display {
534
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
535
            {
536
                if in_plasma_environment(self.connection()).await {
537
                    return PrompterType::Plasma;
538
                }
539
            }
540

            
541
            return PrompterType::GNOME;
542
        }
543

            
544
        PrompterType::Cli
545
    }
546

            
547
27
    pub(crate) fn new(
548
        data_dir: std::path::PathBuf,
549
        pam_socket: Option<std::path::PathBuf>,
550
    ) -> Self {
551
        Self {
552
54
            collections: Arc::new(Mutex::new(HashMap::new())),
553
55
            connection: Arc::new(OnceLock::new()),
554
55
            sessions: Arc::new(Mutex::new(HashMap::new())),
555
56
            session_index: Arc::new(RwLock::new(0)),
556
56
            prompts: Arc::new(Mutex::new(HashMap::new())),
557
56
            prompt_index: Arc::new(RwLock::new(0)),
558
56
            pending_collections: Arc::new(Mutex::new(HashMap::new())),
559
56
            pending_migrations: Arc::new(Mutex::new(HashMap::new())),
560
            data_dir,
561
            pam_socket,
562
56
            prompter_type_override: Arc::new(Mutex::new(None)),
563
        }
564
    }
565

            
566
    pub async fn run(secret: Option<Secret>, request_replacement: bool) -> Result<(), Error> {
567
        // Compute data directory from environment variables
568
        let data_dir = std::env::var_os("XDG_DATA_HOME")
569
            .and_then(|h| if h.is_empty() { None } else { Some(h) })
570
            .map(std::path::PathBuf::from)
571
            .and_then(|p| if p.is_absolute() { Some(p) } else { None })
572
            .or_else(|| {
573
                std::env::var_os("HOME")
574
                    .and_then(|h| if h.is_empty() { None } else { Some(h) })
575
                    .map(std::path::PathBuf::from)
576
                    .map(|p| p.join(".local/share"))
577
            })
578
            .ok_or_else(|| {
579
                Error::IO(std::io::Error::new(
580
                    std::io::ErrorKind::NotFound,
581
                    "No data directory found (XDG_DATA_HOME or HOME)",
582
                ))
583
            })?;
584

            
585
        // Compute PAM socket path from environment variable
586
        let pam_socket = std::env::var_os("OO7_PAM_SOCKET").map(std::path::PathBuf::from);
587

            
588
        let service = Self::new(data_dir, pam_socket);
589

            
590
        // Start PAM listener early so it can buffer secrets arriving before
591
        // D-Bus is ready (e.g. during PAM-initiated login startup).
592
        tracing::info!("Starting PAM listener");
593
        let pam_listener = crate::pam_listener::PamListener::new(service.clone());
594
        let pam_listener_replay = pam_listener.clone();
595
        tokio::spawn(async move {
596
            if let Err(e) = pam_listener.start().await {
597
                tracing::error!("PAM listener error: {}", e);
598
            }
599
        });
600

            
601
        let connection = zbus::connection::Builder::session()?
602
            .allow_name_replacements(true)
603
            .replace_existing_names(request_replacement)
604
            .name(oo7::dbus::api::Service::DESTINATION.as_deref().unwrap())?
605
            .serve_at(
606
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
607
                service.clone(),
608
            )?
609
            .build()
610
            .await?;
611

            
612
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
613
        connection
614
            .object_server()
615
            .at(
616
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
617
                InternalInterface::new(service.clone()),
618
            )
619
            .await?;
620

            
621
        // Discover existing keyrings
622
        let discovered_keyrings = service.discover_keyrings(secret.clone()).await?;
623

            
624
        service
625
            .initialize(connection, discovered_keyrings, secret, true)
626
            .await?;
627

            
628
        // Replay any secrets that the PAM listener buffered during startup
629
        pam_listener_replay.replay_buffered_secrets().await;
630

            
631
        Ok(())
632
    }
633

            
634
    #[cfg(any(test, feature = "test-util"))]
635
    #[allow(dead_code)]
636
27
    pub async fn run_with_connection(
637
        connection: zbus::Connection,
638
        data_dir: std::path::PathBuf,
639
        pam_socket: Option<std::path::PathBuf>,
640
        secret: Option<Secret>,
641
    ) -> Result<Self, Error> {
642
27
        let service = Self::new(data_dir, pam_socket);
643

            
644
        // Serve the service at the standard path
645
105
        connection
646
            .object_server()
647
            .at(
648
29
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
649
27
                service.clone(),
650
            )
651
85
            .await?;
652

            
653
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
654
119
        connection
655
            .object_server()
656
            .at(
657
24
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
658
28
                InternalInterface::new(service.clone()),
659
            )
660
65
            .await?;
661

            
662
24
        let default_keyring = if let Some(secret) = secret.clone() {
663
80
            vec![(
664
32
                "default".to_owned(),
665
23
                "Login".to_owned(),
666
33
                oo7::dbus::Service::DEFAULT_COLLECTION.to_owned(),
667
87
                Keyring::Unlocked(UnlockedKeyring::temporary(secret).await?),
668
            )]
669
        } else {
670
12
            vec![]
671
        };
672

            
673
126
        service
674
23
            .initialize(connection, default_keyring, secret, false)
675
118
            .await?;
676
34
        Ok(service)
677
    }
678

            
679
    /// Generate a unique label and alias by checking registered
680
    /// collections and appending a counter if needed. Returns a tuple of
681
    /// (label, alias).
682
25
    fn make_unique_label_and_alias(
683
        collections: &HashMap<OwnedObjectPath, Collection>,
684
        label: &str,
685
        alias: &str,
686
    ) -> (String, String) {
687
        // Sanitize the label to create the path (for checking uniqueness)
688
27
        let base_path = crate::collection::collection_path(label)
689
            .expect("Sanitized label should always produce valid object path");
690
60
        if !collections.contains_key(&base_path) {
691
60
            return (label.to_owned(), alias.to_owned());
692
        }
693

            
694
        // Append counter until we find a unique one
695
4
        let mut counter = 2;
696
4
        loop {
697
8
            let path = crate::collection::collection_path(&format!("{label}{counter}"))
698
                .expect("Sanitized label should always produce valid object path");
699
4
            let new_label = format!("{}{}", label, counter);
700
8
            let new_alias = format!("{}{}", alias, counter);
701

            
702
8
            if !collections.contains_key(&path) {
703
4
                return (new_label, new_alias);
704
            }
705
            counter += 1;
706
        }
707
    }
708

            
709
    /// Discover existing keyrings in the data directory
710
    /// Returns a vector of (name, label, alias, keyring) tuples
711
4
    pub(crate) async fn discover_keyrings(
712
        &self,
713
        secret: Option<Secret>,
714
    ) -> Result<Vec<(String, String, String, Keyring)>, Error> {
715
4
        let mut discovered = Vec::new();
716

            
717
8
        let keyrings_dir = self.data_dir.join("keyrings");
718

            
719
        // Scan for v1 keyrings first
720
8
        let v1_dir = keyrings_dir.join("v1");
721
8
        if v1_dir.exists() {
722
4
            tracing::debug!("Scanning for v1 keyrings in {}", v1_dir.display());
723
16
            if let Ok(mut entries) = tokio::fs::read_dir(&v1_dir).await {
724
20
                while let Ok(Some(entry)) = entries.next_entry().await {
725
4
                    let path = entry.path();
726

            
727
                    // Skip directories and non-.keyring files
728
8
                    if path.is_dir() || path.extension() != Some(std::ffi::OsStr::new("keyring")) {
729
                        continue;
730
                    }
731

            
732
12
                    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
733
8
                        tracing::debug!("Found v1 keyring: {name}");
734

            
735
                        // Try to load the keyring
736
20
                        match self.load_keyring(&path, name, secret.as_ref()).await {
737
4
                            Ok((name, label, alias, keyring)) => {
738
4
                                discovered.push((name, label, alias, keyring))
739
                            }
740
                            Err(e) => tracing::warn!("Failed to load keyring {:?}: {}", path, e),
741
                        }
742
                    }
743
                }
744
            }
745
        }
746

            
747
        // Scan for v0 keyrings
748
8
        if keyrings_dir.exists() {
749
4
            tracing::debug!("Scanning for v0 keyrings in {}", keyrings_dir.display());
750
16
            if let Ok(mut entries) = tokio::fs::read_dir(&keyrings_dir).await {
751
20
                while let Ok(Some(entry)) = entries.next_entry().await {
752
4
                    let path = entry.path();
753

            
754
                    // Skip directories and non-.keyring files
755
8
                    if path.is_dir() || path.extension() != Some(std::ffi::OsStr::new("keyring")) {
756
                        continue;
757
                    }
758

            
759
4
                    if migration::stamp_path(&path).exists() {
760
                        continue;
761
                    }
762

            
763
12
                    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
764
8
                        tracing::debug!("Found v0 keyring: {name}");
765

            
766
                        // Try to load the keyring
767
20
                        match self.load_keyring(&path, name, secret.as_ref()).await {
768
4
                            Ok((name, label, alias, keyring)) => {
769
4
                                discovered.push((name, label, alias, keyring))
770
                            }
771
                            Err(e) => tracing::warn!("Failed to load keyring {:?}: {}", path, e),
772
                        }
773
                    }
774
                }
775
            }
776
        }
777

            
778
        // Discover KWallet keyrings for migration
779
        #[cfg(feature = "kwallet_migration")]
780
        self.discover_kwallet_keyrings(&self.data_dir, secret.as_ref(), &mut discovered)
781
            .await;
782

            
783
8
        let pending_count = self.pending_migrations.lock().await.len();
784

            
785
8
        if discovered.is_empty() && pending_count == 0 {
786
8
            tracing::info!("No keyrings discovered in data directory");
787
        } else {
788
            tracing::info!(
789
                "Discovered {} keyring(s), {pending_count} pending migration(s)",
790
                discovered.len(),
791
            );
792
        }
793

            
794
4
        Ok(discovered)
795
    }
796

            
797
    /// Discover KWallet keyrings for migration
798
    #[cfg(feature = "kwallet_migration")]
799
    async fn discover_kwallet_keyrings(
800
        &self,
801
        data_dir: &std::path::Path,
802
        secret: Option<&Secret>,
803
        discovered: &mut Vec<(String, String, String, Keyring)>,
804
    ) {
805
        let kwallet_dir = data_dir.join("kwalletd");
806

            
807
        if !kwallet_dir.exists() {
808
            tracing::debug!("No kwalletd directory found, skipping KWallet discovery");
809
            return;
810
        }
811

            
812
        tracing::debug!("Scanning for KWallet files in {}", kwallet_dir.display());
813

            
814
        let Ok(mut entries) = tokio::fs::read_dir(&kwallet_dir).await else {
815
            tracing::warn!("Failed to read kwalletd directory");
816
            return;
817
        };
818

            
819
        while let Ok(Some(entry)) = entries.next_entry().await {
820
            let path = entry.path();
821

            
822
            // Only process .kwl files
823
            if path.extension().is_none_or(|ext| ext != "kwl") {
824
                continue;
825
            }
826

            
827
            if migration::stamp_path(&path).exists() {
828
                continue;
829
            }
830

            
831
            let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
832
                continue;
833
            };
834

            
835
            tracing::debug!("Found KWallet file: {name}");
836

            
837
            // Use lowercased name as alias
838
            let alias = name.to_lowercase();
839

            
840
            let label = {
841
                let mut chars = name.chars();
842
                match chars.next() {
843
                    None => String::new(),
844
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
845
                }
846
            };
847

            
848
            let migration = PendingMigration::KWallet {
849
                name: name.to_owned(),
850
                path: path.clone(),
851
                label: label.clone(),
852
                alias: alias.clone(),
853
            };
854

            
855
            if let Some(secret) = secret {
856
                tracing::debug!("Attempting immediate migration of KWallet keyring '{name}'",);
857
                match migration.migrate(&self.data_dir, Some(secret)).await {
858
                    Ok(unlocked) => {
859
                        tracing::info!("Successfully migrated KWallet keyring '{name}' to oo7",);
860
                        discovered.push((
861
                            name.to_owned(),
862
                            label,
863
                            alias,
864
                            Keyring::Unlocked(unlocked),
865
                        ));
866
                        continue;
867
                    }
868
                    Err(e) => {
869
                        tracing::warn!(
870
                            "Failed to migrate KWallet keyring '{name}' at {}: {e}. Creating locked placeholder collection.",
871
                            migration.path().display()
872
                        );
873
                    }
874
                }
875
            }
876

            
877
            // Migration failed or no secret - create locked placeholder and register for
878
            // pending migration
879
            tracing::debug!(
880
                "Creating locked placeholder for KWallet keyring '{name}', will migrate on unlock",
881
            );
882

            
883
            match LockedKeyring::open_at(&self.data_dir, name).await {
884
                Ok(locked) => {
885
                    tracing::debug!(
886
                        "Created locked placeholder for '{name}', adding to pending migrations",
887
                    );
888
                    discovered.push((
889
                        name.to_owned(),
890
                        label.clone(),
891
                        alias.clone(),
892
                        Keyring::Locked(locked),
893
                    ));
894
                    self.pending_migrations
895
                        .lock()
896
                        .await
897
                        .insert(name.to_owned(), migration);
898
                }
899
                Err(e) => {
900
                    tracing::error!("Failed to create placeholder keyring for '{name}': {e}");
901
                }
902
            }
903
        }
904
    }
905

            
906
    /// Load a single keyring from a file path
907
    /// Returns (name, label, alias, keyring)
908
4
    async fn load_keyring(
909
        &self,
910
        path: &std::path::Path,
911
        name: &str,
912
        secret: Option<&Secret>,
913
    ) -> Result<(String, String, String, Keyring), Error> {
914
12
        let alias = if name.eq_ignore_ascii_case(Self::LOGIN_ALIAS) {
915
8
            oo7::dbus::Service::DEFAULT_COLLECTION.to_owned()
916
        } else {
917
8
            name.to_owned().to_lowercase()
918
        };
919

            
920
        // Use name as label (capitalized for consistency with Login)
921
        let label = {
922
8
            let mut chars = name.chars();
923
4
            match chars.next() {
924
                None => String::new(),
925
4
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
926
            }
927
        };
928

            
929
        // Try to load the keyring
930
16
        let keyring = match LockedKeyring::load(path).await {
931
4
            Ok(locked_keyring) => {
932
                // Successfully loaded as v1 keyring
933
8
                if let Some(secret) = secret {
934
8
                    match locked_keyring.unlock(secret.clone()).await {
935
4
                        Ok(unlocked) => {
936
8
                            tracing::info!("Unlocked keyring '{}' from {:?}", name, path);
937
4
                            Keyring::Unlocked(unlocked)
938
                        }
939
4
                        Err(e) => {
940
8
                            tracing::warn!(
941
                                "Failed to unlock keyring '{}' with provided secret: {}. Keeping it locked.",
942
                                name,
943
                                e
944
                            );
945
                            // Reload as locked since unlock consumed it
946
12
                            Keyring::Locked(LockedKeyring::load(path).await?)
947
                        }
948
                    }
949
                } else {
950
8
                    tracing::debug!("No secret provided, keeping keyring '{}' locked", name);
951
4
                    Keyring::Locked(locked_keyring)
952
                }
953
            }
954
8
            Err(oo7::file::Error::VersionMismatch(Some(version)))
955
8
                if version.first() == Some(&0) =>
956
            // v0 is the legacy version
957
            {
958
                // This is a v0 keyring that needs migration
959
2
                tracing::info!(
960
                    "Found legacy v0 keyring '{name}' at {}, registering for migration",
961
                    path.display()
962
                );
963

            
964
                let migration = PendingMigration::V0 {
965
4
                    name: name.to_owned(),
966
4
                    path: path.to_path_buf(),
967
4
                    label: label.clone(),
968
4
                    alias: alias.clone(),
969
                };
970

            
971
8
                tracing::debug!("Attempting immediate migration of v0 keyring '{name}'",);
972
12
                match migration.migrate(&self.data_dir, secret).await {
973
4
                    Ok(unlocked) => {
974
8
                        tracing::info!("Successfully migrated v0 keyring '{name}' to v1",);
975
8
                        return Ok((name.to_owned(), label, alias, Keyring::Unlocked(unlocked)));
976
                    }
977
4
                    Err(e) => {
978
8
                        tracing::warn!(
979
                            "Failed to migrate v0 keyring '{name}': {e}. Creating locked placeholder collection.",
980
                        );
981
                    }
982
                }
983

            
984
                // Migration failed - create locked placeholder and register for
985
                // pending migration
986
4
                tracing::debug!(
987
                    "Creating locked placeholder for v0 keyring '{}', will migrate on unlock",
988
                    name
989
                );
990

            
991
12
                let locked = LockedKeyring::open(name).await?;
992
16
                self.pending_migrations
993
                    .lock()
994
12
                    .await
995
4
                    .insert(name.to_owned(), migration);
996

            
997
4
                Keyring::Locked(locked)
998
            }
999
            Err(e) => {
                return Err(e.into());
            }
        };
8
        Ok((name.to_owned(), label, alias, keyring))
    }
    /// Initialize the service with collections and start client disconnect
    /// handler
28
    pub(crate) async fn initialize(
        &self,
        connection: zbus::Connection,
        mut discovered_keyrings: Vec<(String, String, String, Keyring)>, /* (name, label, alias,
                                                                          * keyring) */
        secret: Option<Secret>,
        auto_create_default: bool,
    ) -> Result<(), Error> {
52
        self.connection.set(connection.clone()).unwrap();
28
        let object_server = connection.object_server();
52
        let mut collections = self.collections.lock().await;
        // Check if we have a default collection
104
        let has_default = discovered_keyrings.iter().any(|(_, _, alias, _)| {
23
            alias == oo7::dbus::Service::DEFAULT_COLLECTION || alias == Self::LOGIN_ALIAS
        });
29
        if !has_default && auto_create_default {
            tracing::info!("No default collection found, creating 'Login' keyring");
            let keyring = if let Some(secret) = secret {
                UnlockedKeyring::open_at(&self.data_dir, Self::LOGIN_ALIAS, Some(secret))
                    .await
                    .map(Keyring::Unlocked)
            } else {
                LockedKeyring::open_at(&self.data_dir, Self::LOGIN_ALIAS)
                    .await
                    .map(Keyring::Locked)
            };
            let keyring = keyring.inspect_err(|e| {
                tracing::error!("Failed to create default Login keyring: {}", e);
            })?;
            let is_locked = if keyring.is_locked() {
                "locked"
            } else {
                "unlocked"
            };
            discovered_keyrings.push((
                Self::LOGIN_ALIAS.to_owned(),
                "Login".to_owned(),
                oo7::dbus::Service::DEFAULT_COLLECTION.to_owned(),
                keyring,
            ));
            tracing::info!("Created default 'Login' collection ({})", is_locked);
        }
        // Build all collections under the lock, then register on D-Bus after
        // releasing it to avoid deadlocks with incoming calls.
23
        let mut built_collections: Vec<(Collection, String)> = Vec::new();
104
        for (name, label, alias, keyring) in discovered_keyrings {
63
            tracing::info!("Setting up collection '{name}' (alias: {alias}).");
38
            let (unique_label, unique_alias) =
                Self::make_unique_label_and_alias(&collections, &label, &alias);
66
            let collection =
                Collection::new(&name, &unique_label, &unique_alias, self.clone(), keyring).await;
60
            collections.insert(collection.path().to_owned().into(), collection.clone());
26
            built_collections.push((collection, unique_alias));
        }
        // Always create session collection (always temporary)
        let session_collection = Collection::new(
            "session",
            "session",
26
            oo7::dbus::Service::SESSION_COLLECTION,
49
            self.clone(),
80
            Keyring::Unlocked(UnlockedKeyring::temporary(Secret::random().unwrap()).await?),
        )
75
        .await;
58
        collections.insert(
51
            session_collection.path().to_owned().into(),
24
            session_collection.clone(),
        );
24
        drop(collections);
        // Now register on D-Bus and dispatch items without holding the lock
81
        for (collection, alias) in &built_collections {
108
            collection.dispatch_items().await?;
139
            object_server
32
                .at(collection.path(), collection.clone())
136
                .await?;
63
            if alias == oo7::dbus::Service::DEFAULT_COLLECTION {
173
                object_server
64
                    .at(DEFAULT_COLLECTION_ALIAS_PATH, collection.clone())
135
                    .await?;
            }
        }
33
        let session_path = session_collection.path().to_owned();
64
        object_server.at(&session_path, session_collection).await?;
        // Spawn client disconnect handler
31
        let service = self.clone();
95
        tokio::spawn(async move { service.on_client_disconnect().await });
        // Spawn stale session cleanup task
33
        let service = self.clone();
87
        tokio::spawn(async move { service.cleanup_stale_sessions().await });
33
        Ok(())
    }
126
    async fn on_client_disconnect(&self) -> zbus::Result<()> {
159
        let rule = zbus::MatchRule::builder()
32
            .msg_type(zbus::message::Type::Signal)
            .sender("org.freedesktop.DBus")?
            .interface("org.freedesktop.DBus")?
            .member("NameOwnerChanged")?
            .arg(2, "")?
            .build();
64
        let mut stream = zbus::MessageStream::for_match_rule(rule, self.connection(), None).await?;
78
        while let Some(message) = stream.try_next().await? {
            let body = message.body();
            let Ok((_name, old_owner, new_owner)) =
                body.deserialize::<(String, Optional<UniqueName<'_>>, Optional<UniqueName<'_>>)>()
            else {
                continue;
            };
            debug_assert!(new_owner.is_none()); // We enforce that in the matching rule
            let old_owner = old_owner
                .as_ref()
                .expect("A disconnected client requires an old_owner");
            if let Some(session) = self.session_from_sender(old_owner).await {
                let client_name = match session.peer_name() {
                    Some(name) => format!("{old_owner} ({name})"),
                    None => old_owner.to_string(),
                };
                session.mark_stale().await;
                tracing::info!(
                    "Client {} disconnected. Session: {} marked for cleanup.",
                    client_name,
                    session.path()
                );
            }
        }
        Ok(())
    }
103
    async fn cleanup_stale_sessions(&self) {
52
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
        loop {
108
            interval.tick().await;
            let stale_paths: Vec<_> = {
42
                let sessions = self.sessions.lock().await;
25
                let mut paths = Vec::new();
58
                for (path, session) in sessions.iter() {
                    if session.is_stale().await {
                        paths.push(path.clone());
                    }
                }
24
                paths
            };
43
            for path in stale_paths {
                if let Some(session) = self.session(&path).await {
                    match session.close().await {
                        Ok(_) => tracing::info!("Stale session {} cleaned up.", path),
                        Err(err) => {
                            tracing::error!("Failed to clean up stale session {}: {}", path, err)
                        }
                    }
                }
            }
        }
    }
10
    pub async fn set_locked(
        &self,
        locked: bool,
        objects: &[OwnedObjectPath],
    ) -> Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>), ServiceError> {
10
        let mut without_prompt = Vec::new();
10
        let mut with_prompt = Vec::new();
20
        let collections = self.collections.lock().await;
40
        for object in objects {
50
            let resolved = Self::resolve_alias(&collections, object)
40
                .await
40
                .unwrap_or_else(|| object.clone());
10
            let mut found = false;
20
            for (path, collection) in collections.iter() {
20
                let collection_locked = collection.is_locked().await;
10
                if resolved == *path {
8
                    found = true;
8
                    if collection_locked == locked {
11
                        tracing::debug!(
                            "Collection: {} is already {}.",
                            resolved,
                            if locked { "locked" } else { "unlocked" }
                        );
12
                        without_prompt.push(resolved.clone());
8
                    } else if locked {
                        // Locking never requires a prompt
26
                        collection.set_locked(true, None).await?;
8
                        without_prompt.push(resolved.clone());
                    } else {
                        // Unlocking may require a prompt
16
                        with_prompt.push(resolved.clone());
                    }
                    break;
27
                } else if let Some(item) = collection.item_from_path(&resolved).await {
8
                    found = true;
                    // If collection is locked, can't perform any item lock/unlock operations
8
                    if collection_locked {
                        // Unlocking an item when collection is locked requires unlocking collection
6
                        if !locked {
8
                            with_prompt.push(resolved.clone());
                        } else {
                            // Can't lock an item when collection is locked
4
                            return Err(ServiceError::IsLocked(format!(
                                "Cannot lock item {} when collection is locked",
                                resolved
                            )));
                        }
24
                    } else if locked == item.is_locked().await {
5
                        tracing::debug!(
                            "Item: {} is already {}.",
                            resolved,
                            if locked { "locked" } else { "unlocked" }
                        );
12
                        without_prompt.push(resolved.clone());
                    } else {
24
                        let keyring = collection.keyring.read().await;
8
                        match keyring.as_ref() {
16
                            Some(k) if !k.is_locked() => {
24
                                item.set_locked(locked, k.as_unlocked()).await?;
8
                                without_prompt.push(resolved.clone());
                            }
                            _ => {
                                if locked {
                                    return Err(ServiceError::IsLocked(format!(
                                        "Cannot lock item {} when collection is locked",
                                        resolved
                                    )));
                                } else {
                                    with_prompt.push(resolved.clone());
                                }
                            }
                        }
                    }
                    break;
                }
            }
10
            if !found {
8
                tracing::warn!("Object: {} does not exist.", object);
            }
        }
10
        Ok((without_prompt, with_prompt))
    }
36
    pub fn connection(&self) -> &zbus::Connection {
32
        self.connection.get().unwrap()
    }
36
    pub fn object_server(&self) -> &zbus::ObjectServer {
32
        self.connection().object_server()
    }
10
    async fn resolve_alias(
        collections: &HashMap<OwnedObjectPath, Collection>,
        path: &ObjectPath<'_>,
    ) -> Option<OwnedObjectPath> {
30
        let alias = path.strip_prefix("/org/freedesktop/secrets/aliases/")?;
        let alias_to_find = if alias == Self::LOGIN_ALIAS {
            oo7::dbus::Service::DEFAULT_COLLECTION
        } else {
            alias
        };
        for (real_path, collection) in collections.iter() {
            if collection.alias().await == alias_to_find {
                return Some(real_path.clone());
            }
        }
        None
    }
52
    pub async fn collection_from_path(&self, path: &ObjectPath<'_>) -> Option<Collection> {
26
        let collections = self.collections.lock().await;
26
        if let Some(collection) = collections.get(path).cloned() {
14
            return Some(collection);
        }
12
        let resolved = Self::resolve_alias(&collections, path).await?;
        collections.get(&resolved).cloned()
    }
130
    pub async fn session_index(&self) -> u32 {
67
        let mut guard = self.session_index.write().await;
67
        *guard += 1;
64
        *guard
    }
    async fn session_from_sender(&self, sender: &UniqueName<'_>) -> Option<Session> {
        let sessions = self.sessions.lock().await;
        sessions.values().find(|s| s.sender() == sender).cloned()
    }
    pub async fn peer_display_name(&self, sender: &UniqueName<'_>) -> String {
        match self.session_from_sender(sender).await {
            Some(session) => match session.peer_name() {
                Some(name) => format!("{sender} ({name})"),
                None => sender.to_string(),
            },
            None => sender.to_string(),
        }
    }
64
    pub async fn session(&self, path: &ObjectPath<'_>) -> Option<Session> {
35
        let session = self.sessions.lock().await.get(path).cloned()?;
15
        session.unmark_stale().await;
15
        Some(session)
    }
24
    pub async fn remove_session(&self, path: &ObjectPath<'_>) {
12
        self.sessions.lock().await.remove(path);
    }
37
    pub async fn remove_collection(&self, path: &ObjectPath<'_>) {
16
        self.collections.lock().await.remove(path);
15
        if let Ok(signal_emitter) =
            self.signal_emitter(oo7::dbus::api::Service::PATH.as_deref().unwrap())
        {
18
            let _ = self.collections_changed(&signal_emitter).await;
        }
    }
51
    pub async fn prompt_index(&self) -> u32 {
25
        let mut guard = self.prompt_index.write().await;
25
        *guard += 1;
25
        *guard
    }
46
    pub async fn prompt(&self, path: &ObjectPath<'_>) -> Option<Prompt> {
22
        self.prompts.lock().await.get(path).cloned()
    }
41
    pub async fn remove_prompt(&self, path: &ObjectPath<'_>) {
22
        self.prompts.lock().await.remove(path);
        // Also clean up pending collection if it exists
11
        self.pending_collections.lock().await.remove(path);
    }
16
    pub async fn register_prompt(&self, path: OwnedObjectPath, prompt: Prompt) {
8
        self.prompts.lock().await.insert(path, prompt);
    }
10
    pub async fn pending_collection(
        &self,
        prompt_path: &ObjectPath<'_>,
    ) -> Option<(String, String)> {
40
        self.pending_collections
            .lock()
27
            .await
9
            .get(prompt_path)
            .cloned()
    }
9
    pub async fn create_collection_with_secret(
        &self,
        label: &str,
        alias: &str,
        secret: Option<Secret>,
    ) -> Result<OwnedObjectPath, ServiceError> {
        // Create a persistent keyring with the provided secret
68
        let keyring = UnlockedKeyring::open_at(&self.data_dir, &label.to_lowercase(), secret)
29
            .await
18
            .map_err(|err| custom_service_error(&format!("Failed to create keyring: {err}")))?;
        // Write the keyring file to disk immediately
50
        keyring
            .write()
37
            .await
9
            .map_err(|err| custom_service_error(&format!("Failed to write keyring file: {err}")))?;
9
        let keyring = Keyring::Unlocked(keyring);
10
        let name = label.to_lowercase();
        // Create the collection with unique label and alias
9
        let (unique_label, unique_alias) = {
19
            let collections = self.collections.lock().await;
19
            Self::make_unique_label_and_alias(&collections, label, alias)
        };
29
        let collection =
            Collection::new(&name, &unique_label, &unique_alias, self.clone(), keyring).await;
19
        let collection_path: OwnedObjectPath = collection.path().to_owned().into();
        // Register with object server
37
        self.object_server()
9
            .at(collection.path(), collection.clone())
30
            .await?;
        // Add to collections
40
        self.collections
            .lock()
27
            .await
9
            .insert(collection_path.clone(), collection);
        // Emit CollectionCreated signal
10
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
9
        let signal_emitter = self.signal_emitter(service_path)?;
19
        Service::collection_created(&signal_emitter, &collection_path).await?;
        // Emit PropertiesChanged for Collections property to invalidate client cache
9
        self.collections_changed(&signal_emitter).await?;
16
        tracing::info!(
            "Collection `{}` created with label '{}'",
            collection_path,
            label
        );
9
        Ok(collection_path)
    }
9
    pub async fn complete_collection_creation(
        &self,
        prompt_path: &ObjectPath<'_>,
        secret: Option<Secret>,
    ) -> Result<OwnedObjectPath, ServiceError> {
18
        let Some((label, alias)) = self.pending_collection(prompt_path).await else {
8
            return Err(ServiceError::NoSuchObject(format!(
                "No pending collection for prompt `{prompt_path}`"
            )));
        };
40
        let collection_path = self
19
            .create_collection_with_secret(&label, &alias, secret)
38
            .await?;
20
        self.pending_collections.lock().await.remove(prompt_path);
10
        Ok(collection_path)
    }
52
    pub fn signal_emitter<'a, P>(
        &self,
        path: P,
    ) -> Result<zbus::object_server::SignalEmitter<'a>, oo7::dbus::ServiceError>
    where
        P: TryInto<ObjectPath<'a>>,
        P::Error: Into<zbus::Error>,
    {
103
        let signal_emitter = zbus::object_server::SignalEmitter::new(self.connection(), path)?;
50
        Ok(signal_emitter)
    }
    /// Extract the collection label from a list of object paths
    /// The objects can be either collections or items
32
    async fn extract_label_from_objects(&self, objects: &[OwnedObjectPath]) -> String {
16
        if objects.is_empty() {
            return String::new();
        }
        // Check if at least one of the objects is a Collection
32
        for object in objects {
24
            if let Some(collection) = self.collection_from_path(object).await {
16
                return collection.label().await;
            }
        }
        // Get the collection path from the first item
        // assumes all items are from the same collection
12
        if let Some(path_str) = objects.first().and_then(|p| p.as_str().rsplit_once('/')) {
4
            let collection_path = path_str.0;
12
            if let Ok(obj_path) = ObjectPath::try_from(collection_path)
8
                && let Some(collection) = self.collection_from_path(&obj_path).await
            {
8
                return collection.label().await;
            }
        }
        String::new()
    }
    /// Extract the collection from a list of object paths
    /// The objects can be either collections or items
8
    async fn extract_collection_from_objects(
        &self,
        objects: &[OwnedObjectPath],
    ) -> Option<Collection> {
16
        if objects.is_empty() {
            return None;
        }
        // Check if at least one of the objects is a Collection
32
        for object in objects {
24
            if let Some(collection) = self.collection_from_path(object).await {
8
                return Some(collection);
            }
        }
        // Get the collection path from the first item
        // (assumes all items are from the same collection)
12
        let path = objects
            .first()
            .unwrap()
            .as_str()
            .rsplit_once('/')
8
            .map(|(parent, _)| parent)?;
12
        self.collection_from_path(&ObjectPath::try_from(path).unwrap())
8
            .await
    }
    /// Attempt to migrate pending keyrings with the provided secret
    /// Returns a list of successfully migrated keyring names
28
    pub async fn migrate_pending_keyrings(&self, secret: &Secret) -> Vec<String> {
4
        let mut migrated = Vec::new();
8
        let mut pending = self.pending_migrations.lock().await;
4
        let mut to_remove = Vec::new();
16
        for (name, migration) in pending.iter() {
8
            tracing::debug!("Attempting to migrate pending keyring: {name}");
16
            match migration.migrate(&self.data_dir, Some(secret)).await {
4
                Ok(unlocked) => {
4
                    let label = migration.label();
4
                    let alias = migration.alias();
                    // Create a collection for this migrated keyring with unique label and alias
4
                    let (unique_label, unique_alias) = {
8
                        let collections = self.collections.lock().await;
8
                        Self::make_unique_label_and_alias(&collections, label, alias)
                    };
4
                    let keyring = Keyring::Unlocked(unlocked);
12
                    let collection =
                        Collection::new(name, &unique_label, &unique_alias, self.clone(), keyring)
20
                            .await;
4
                    let collection_path: OwnedObjectPath = collection.path().to_owned().into();
                    // Dispatch items
12
                    if let Err(e) = collection.dispatch_items().await {
                        tracing::error!(
                            "Failed to dispatch items for migrated keyring '{name}': {e}",
                        );
                        continue;
                    }
16
                    if let Err(e) = self
                        .object_server()
4
                        .at(collection.path(), collection.clone())
16
                        .await
                    {
                        tracing::error!(
                            "Failed to register migrated collection '{name}' with object server: {e}",
                        );
                        continue;
                    }
20
                    self.collections
                        .lock()
16
                        .await
8
                        .insert(collection_path.clone(), collection.clone());
4
                    if alias == oo7::dbus::Service::DEFAULT_COLLECTION
                        && let Err(e) = self
                            .object_server()
                            .at(DEFAULT_COLLECTION_ALIAS_PATH, collection)
                            .await
                    {
                        tracing::error!(
                            "Failed to register default alias for migrated collection '{name}': {e}",
                        );
                    }
8
                    if let Ok(signal_emitter) =
                        self.signal_emitter(oo7::dbus::api::Service::PATH.as_ref().unwrap())
                    {
8
                        let _ =
                            Service::collection_created(&signal_emitter, &collection_path).await;
12
                        let _ = self.collections_changed(&signal_emitter).await;
                    }
8
                    tracing::info!("Migrated keyring '{name}' added as collection",);
8
                    migrated.push(name.clone());
4
                    to_remove.push(name.clone());
                }
                Err(e) => {
                    tracing::debug!(
                        "Failed to migrate keyring '{name}' found at {} with provided secret: {e}",
                        migration.path().display()
                    );
                }
            }
        }
4
        for name in &to_remove {
8
            pending.remove(name);
        }
4
        migrated
    }
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod unencrypted_tests;