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
use crate::{
26
    collection::Collection,
27
    error::{Error, custom_service_error},
28
    prompt::{Prompt, PromptAction, PromptRole},
29
    session::Session,
30
};
31

            
32
const DEFAULT_COLLECTION_ALIAS_PATH: ObjectPath<'static> =
33
    ObjectPath::from_static_str_unchecked("/org/freedesktop/secrets/aliases/default");
34

            
35
#[derive(Debug, Default, Clone)]
36
pub struct Service {
37
    // Properties
38
    pub(crate) collections: Arc<Mutex<HashMap<OwnedObjectPath, Collection>>>,
39
    // Other attributes
40
    connection: Arc<OnceLock<zbus::Connection>>,
41
    // sessions mapped to their corresponding object path on the bus
42
    sessions: Arc<Mutex<HashMap<OwnedObjectPath, Session>>>,
43
    session_index: Arc<RwLock<u32>>,
44
    // prompts mapped to their corresponding object path on the bus
45
    prompts: Arc<Mutex<HashMap<OwnedObjectPath, Prompt>>>,
46
    prompt_index: Arc<RwLock<u32>>,
47
    // pending collection creations: prompt_path -> (label, alias)
48
    pending_collections: Arc<Mutex<HashMap<OwnedObjectPath, (String, String)>>>,
49
    // pending v0 keyring migrations: name -> (path, label, alias)
50
    #[allow(clippy::type_complexity)]
51
    pub(crate) pending_migrations:
52
        Arc<Mutex<HashMap<String, (std::path::PathBuf, String, String)>>>,
53
}
54

            
55
#[zbus::interface(name = "org.freedesktop.Secret.Service")]
56
impl Service {
57
    #[zbus(out_args("output", "result"))]
58
5
    pub async fn open_session(
59
        &self,
60
        algorithm: Algorithm,
61
        input: Value<'_>,
62
        #[zbus(header)] header: zbus::message::Header<'_>,
63
        #[zbus(object_server)] object_server: &zbus::ObjectServer,
64
    ) -> Result<(OwnedValue, OwnedObjectPath), ServiceError> {
65
8
        let (public_key, aes_key) = match algorithm {
66
5
            Algorithm::Plain => (None, None),
67
            Algorithm::Encrypted => {
68
4
                let client_public_key = Key::try_from(input).map_err(|err| {
69
                    custom_service_error(&format!(
70
                        "Input Value could not be converted into a Key {err}."
71
                    ))
72
                })?;
73
4
                let private_key = Key::generate_private_key().map_err(|err| {
74
                    custom_service_error(&format!("Failed to generate private key {err}."))
75
                })?;
76
                (
77
4
                    Some(Key::generate_public_key(&private_key).map_err(|err| {
78
                        custom_service_error(&format!("Failed to generate public key {err}."))
79
                    })?),
80
2
                    Some(
81
4
                        Key::generate_aes_key(&private_key, &client_public_key).map_err(|err| {
82
                            custom_service_error(&format!("Failed to generate aes key {err}."))
83
                        })?,
84
                    ),
85
                )
86
            }
87
        };
88

            
89
14
        let sender = if let Some(s) = header.sender() {
90
            s.to_owned()
91
        } else {
92
            #[cfg(test)]
93
            {
94
                // For p2p test connections, use a dummy sender since p2p connections
95
                // don't have a bus to assign unique names
96
                UniqueName::try_from(":p2p.test").unwrap().into()
97
            }
98
            #[cfg(not(test))]
99
            {
100
                return Err(custom_service_error("Failed to get sender from header."));
101
            }
102
        };
103

            
104
9
        tracing::info!("Client {} connected", sender);
105

            
106
8
        let session = Session::new(aes_key.map(Arc::new), self.clone(), sender).await;
107
9
        let path = OwnedObjectPath::from(session.path().clone());
108

            
109
30
        self.sessions
110
            .lock()
111
18
            .await
112
12
            .insert(path.clone(), session.clone());
113

            
114
6
        object_server.at(&path, session).await?;
115

            
116
6
        let service_key = public_key
117
6
            .map(OwnedValue::from)
118
13
            .unwrap_or_else(|| Value::new::<Vec<u8>>(vec![]).try_into_owned().unwrap());
119

            
120
4
        Ok((service_key, path))
121
    }
122

            
123
    #[zbus(out_args("collection", "prompt"))]
124
2
    pub async fn create_collection(
125
        &self,
126
        properties: Properties,
127
        alias: &str,
128
    ) -> Result<(OwnedObjectPath, ObjectPath<'_>), ServiceError> {
129
4
        let label = properties.label().to_owned();
130
4
        let alias = alias.to_owned();
131

            
132
        // Create a prompt to get the password for the new collection
133
        let prompt = Prompt::new(
134
4
            self.clone(),
135
            PromptRole::CreateCollection,
136
2
            label.clone(),
137
2
            None,
138
        )
139
6
        .await;
140
4
        let prompt_path = OwnedObjectPath::from(prompt.path().clone());
141

            
142
        // Store the collection metadata for later creation
143
8
        self.pending_collections
144
            .lock()
145
6
            .await
146
2
            .insert(prompt_path.clone(), (label, alias));
147

            
148
        // Create the collection creation action
149
4
        let service = self.clone();
150
2
        let creation_prompt_path = prompt_path.clone();
151
10
        let action = PromptAction::new(move |secret: Secret| async move {
152
8
            let collection_path = service
153
4
                .complete_collection_creation(&creation_prompt_path, secret)
154
8
                .await?;
155

            
156
4
            Ok(Value::new(collection_path).try_into_owned().unwrap())
157
        });
158

            
159
2
        prompt.set_action(action).await;
160

            
161
        // Register the prompt
162
10
        self.prompts
163
            .lock()
164
6
            .await
165
4
            .insert(prompt_path.clone(), prompt.clone());
166

            
167
2
        self.object_server().at(&prompt_path, prompt).await?;
168

            
169
2
        tracing::debug!("CreateCollection prompt created at `{}`", prompt_path);
170

            
171
        // Return empty collection path and the prompt path
172
4
        Ok((OwnedObjectPath::default(), prompt_path.into()))
173
    }
174

            
175
    #[zbus(out_args("unlocked", "locked"))]
176
2
    pub async fn search_items(
177
        &self,
178
        attributes: HashMap<String, String>,
179
    ) -> Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>), ServiceError> {
180
2
        let mut unlocked = Vec::new();
181
2
        let mut locked = Vec::new();
182
4
        let collections = self.collections.lock().await;
183

            
184
6
        for (_path, collection) in collections.iter() {
185
8
            let items = collection.search_inner_items(&attributes).await?;
186
4
            for item in items {
187
8
                if item.is_locked().await {
188
4
                    locked.push(item.path().clone().into());
189
                } else {
190
4
                    unlocked.push(item.path().clone().into());
191
                }
192
            }
193
        }
194

            
195
4
        if unlocked.is_empty() && locked.is_empty() {
196
4
            tracing::debug!(
197
                "Items with attributes {:?} does not exist in any collection.",
198
                attributes
199
            );
200
        } else {
201
4
            tracing::debug!("Items with attributes {:?} found.", attributes);
202
        }
203

            
204
2
        Ok((unlocked, locked))
205
    }
206

            
207
    #[zbus(out_args("unlocked", "prompt"))]
208
2
    pub async fn unlock(
209
        &self,
210
        objects: Vec<OwnedObjectPath>,
211
    ) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
212
4
        let (unlocked, not_unlocked) = self.set_locked(false, &objects).await?;
213
4
        if !not_unlocked.is_empty() {
214
            // Extract the label and collection before creating the prompt
215
4
            let label = self.extract_label_from_objects(&not_unlocked).await;
216
4
            let collection = self.extract_collection_from_objects(&not_unlocked).await;
217

            
218
4
            let prompt = Prompt::new(self.clone(), PromptRole::Unlock, label, collection).await;
219
4
            let path = OwnedObjectPath::from(prompt.path().clone());
220

            
221
            // Create the unlock action
222
2
            let service = self.clone();
223
12
            let action = PromptAction::new(move |secret: Secret| async move {
224
                // The prompter will handle secret validation
225
                // Here we just perform the unlock operation
226
4
                let collections = service.collections.lock().await;
227
8
                for object in &not_unlocked {
228
                    // Try to find as collection first
229
4
                    if let Some(collection) = collections.get(object) {
230
6
                        let _ = collection.set_locked(false, Some(secret.clone())).await;
231
                    } else {
232
                        // Try to find as item within collections
233
6
                        for (_path, collection) in collections.iter() {
234
4
                            if let Some(item) = collection.item_from_path(object).await {
235
                                // If the collection is locked, unlock it
236
4
                                if collection.is_locked().await {
237
                                    let _ =
238
6
                                        collection.set_locked(false, Some(secret.clone())).await;
239
                                } else {
240
                                    // Collection is already unlocked, just unlock the item
241
                                    let keyring = collection.keyring.read().await;
242
                                    let _ = item
243
                                        .set_locked(false, keyring.as_ref().unwrap().as_unlocked())
244
                                        .await;
245
                                }
246
                                break;
247
                            }
248
                        }
249
                    }
250
                }
251
2
                Ok(Value::new(not_unlocked).try_into_owned().unwrap())
252
            });
253

            
254
2
            prompt.set_action(action).await;
255

            
256
10
            self.prompts
257
                .lock()
258
6
                .await
259
4
                .insert(path.clone(), prompt.clone());
260

            
261
2
            self.object_server().at(&path, prompt).await?;
262
2
            return Ok((unlocked, path));
263
        }
264

            
265
2
        Ok((unlocked, OwnedObjectPath::default()))
266
    }
267

            
268
    #[zbus(out_args("locked", "Prompt"))]
269
2
    pub async fn lock(
270
        &self,
271
        objects: Vec<OwnedObjectPath>,
272
    ) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
273
        // set_locked now handles locking directly (without prompts)
274
4
        let (locked, not_locked) = self.set_locked(true, &objects).await?;
275
        // Locking never requires prompts, so not_locked should always be empty
276
        debug_assert!(
277
            not_locked.is_empty(),
278
            "Lock operation should never require prompts"
279
        );
280
2
        Ok((locked, OwnedObjectPath::default()))
281
    }
282

            
283
    #[zbus(out_args("secrets"))]
284
2
    pub async fn get_secrets(
285
        &self,
286
        items: Vec<OwnedObjectPath>,
287
        session: OwnedObjectPath,
288
    ) -> Result<HashMap<OwnedObjectPath, DBusSecretInner>, ServiceError> {
289
2
        let mut secrets = HashMap::new();
290
4
        let collections = self.collections.lock().await;
291

            
292
8
        'outer: for (_path, collection) in collections.iter() {
293
8
            for item in &items {
294
6
                if let Some(item) = collection.item_from_path(item).await {
295
6
                    match item.get_secret(session.clone()).await {
296
2
                        Ok((secret,)) => {
297
4
                            secrets.insert(item.path().clone().into(), secret);
298
                            // To avoid iterating through all the remaining collections, if the
299
                            // items secrets are already retrieved.
300
2
                            if secrets.len() == items.len() {
301
                                break 'outer;
302
                            }
303
                        }
304
                        // Avoid erroring out if an item is locked.
305
                        Err(ServiceError::IsLocked(_)) => {
306
                            continue;
307
                        }
308
2
                        Err(err) => {
309
2
                            return Err(err);
310
                        }
311
                    };
312
                }
313
            }
314
        }
315

            
316
2
        Ok(secrets)
317
    }
318

            
319
    #[zbus(out_args("collection"))]
320
8
    pub async fn read_alias(&self, name: &str) -> Result<OwnedObjectPath, ServiceError> {
321
        // Map "login" alias to "default" for compatibility with gnome-keyring
322
6
        let alias_to_find = if name == Self::LOGIN_ALIAS {
323
            oo7::dbus::Service::DEFAULT_COLLECTION
324
        } else {
325
2
            name
326
        };
327

            
328
2
        let collections = self.collections.lock().await;
329

            
330
8
        for (path, collection) in collections.iter() {
331
6
            if collection.alias().await == alias_to_find {
332
2
                tracing::debug!("Collection: {} found for alias: {}.", path, name);
333
4
                return Ok(path.to_owned());
334
            }
335
        }
336

            
337
2
        tracing::info!("Collection with alias {} does not exist.", name);
338

            
339
4
        Ok(OwnedObjectPath::default())
340
    }
341

            
342
2
    pub async fn set_alias(
343
        &self,
344
        name: &str,
345
        collection: OwnedObjectPath,
346
    ) -> Result<(), ServiceError> {
347
4
        let collections = self.collections.lock().await;
348

            
349
6
        for (path, other_collection) in collections.iter() {
350
4
            if *path == collection {
351
2
                other_collection.set_alias(name).await;
352

            
353
2
                tracing::info!("Collection: {} alias updated to {}.", collection, name);
354
2
                return Ok(());
355
            }
356
        }
357

            
358
2
        tracing::info!("Collection: {} does not exist.", collection);
359

            
360
4
        Err(ServiceError::NoSuchObject(format!(
361
            "The collection: {collection} does not exist.",
362
        )))
363
    }
364

            
365
    #[zbus(property, name = "Collections")]
366
12
    pub async fn collections(&self) -> Vec<OwnedObjectPath> {
367
6
        self.collections.lock().await.keys().cloned().collect()
368
    }
369

            
370
    #[zbus(signal, name = "CollectionCreated")]
371
    pub async fn collection_created(
372
2
        signal_emitter: &SignalEmitter<'_>,
373
2
        collection: &ObjectPath<'_>,
374
    ) -> zbus::Result<()>;
375

            
376
    #[zbus(signal, name = "CollectionDeleted")]
377
    pub async fn collection_deleted(
378
2
        signal_emitter: &SignalEmitter<'_>,
379
2
        collection: &ObjectPath<'_>,
380
    ) -> zbus::Result<()>;
381

            
382
    #[zbus(signal, name = "CollectionChanged")]
383
    pub async fn collection_changed(
384
2
        signal_emitter: &SignalEmitter<'_>,
385
2
        collection: &ObjectPath<'_>,
386
    ) -> zbus::Result<()>;
387
}
388

            
389
impl Service {
390
    const LOGIN_ALIAS: &str = "login";
391

            
392
    pub async fn run(secret: Option<Secret>, request_replacement: bool) -> Result<(), Error> {
393
        let service = Self::default();
394

            
395
        let connection = zbus::connection::Builder::session()?
396
            .allow_name_replacements(true)
397
            .replace_existing_names(request_replacement)
398
            .name(oo7::dbus::api::Service::DESTINATION.as_deref().unwrap())?
399
            .serve_at(
400
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
401
                service.clone(),
402
            )?
403
            .build()
404
            .await?;
405

            
406
        // Discover existing keyrings
407
        let discovered_keyrings = service.discover_keyrings(secret).await?;
408

            
409
        service
410
            .initialize(connection, discovered_keyrings, true)
411
            .await?;
412

            
413
        // Start PAM listener
414
        tracing::info!("Starting PAM listener");
415
        let pam_listener = crate::pam_listener::PamListener::new(service.clone());
416
        tokio::spawn(async move {
417
            if let Err(e) = pam_listener.start().await {
418
                tracing::error!("PAM listener error: {}", e);
419
            }
420
        });
421

            
422
        Ok(())
423
    }
424

            
425
    #[cfg(test)]
426
5
    pub async fn run_with_connection(
427
        connection: zbus::Connection,
428
        secret: Option<Secret>,
429
    ) -> Result<Self, Error> {
430
5
        let service = Self::default();
431

            
432
        // Serve the service at the standard path
433
24
        connection
434
            .object_server()
435
            .at(
436
5
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
437
7
                service.clone(),
438
            )
439
15
            .await?;
440

            
441
11
        let default_keyring = if let Some(secret) = secret {
442
15
            vec![(
443
6
                "Login".to_owned(),
444
6
                oo7::dbus::Service::DEFAULT_COLLECTION.to_owned(),
445
17
                Keyring::Unlocked(UnlockedKeyring::temporary(secret).await?),
446
            )]
447
        } else {
448
4
            vec![]
449
        };
450

            
451
17
        service
452
5
            .initialize(connection, default_keyring, false)
453
21
            .await?;
454
6
        Ok(service)
455
    }
456

            
457
    /// Discover existing keyrings in the data directory
458
    /// Returns a vector of (label, alias, keyring) tuples
459
2
    pub(crate) async fn discover_keyrings(
460
        &self,
461
        secret: Option<Secret>,
462
    ) -> Result<Vec<(String, String, Keyring)>, Error> {
463
2
        let mut discovered = Vec::new();
464

            
465
        // Get data directory using the same logic as oo7::file::api::data_dir()
466
4
        let data_dir = std::env::var_os("XDG_DATA_HOME")
467
6
            .and_then(|h| if h.is_empty() { None } else { Some(h) })
468
2
            .map(std::path::PathBuf::from)
469
6
            .and_then(|p| if p.is_absolute() { Some(p) } else { None })
470
2
            .or_else(|| {
471
                std::env::var_os("HOME")
472
                    .and_then(|h| if h.is_empty() { None } else { Some(h) })
473
                    .map(std::path::PathBuf::from)
474
                    .map(|p| p.join(".local/share"))
475
            });
476

            
477
2
        let Some(data_dir) = data_dir else {
478
            tracing::warn!("No data directory found, skipping keyring discovery");
479
            return Ok(discovered);
480
        };
481

            
482
4
        let keyrings_dir = data_dir.join("keyrings");
483

            
484
        // Scan for v1 keyrings first
485
4
        let v1_dir = keyrings_dir.join("v1");
486
4
        if v1_dir.exists() {
487
2
            tracing::debug!("Scanning for v1 keyrings in {}", v1_dir.display());
488
8
            if let Ok(mut entries) = tokio::fs::read_dir(&v1_dir).await {
489
10
                while let Ok(Some(entry)) = entries.next_entry().await {
490
2
                    let path = entry.path();
491

            
492
                    // Skip directories and non-.keyring files
493
4
                    if path.is_dir() || path.extension() != Some(std::ffi::OsStr::new("keyring")) {
494
                        continue;
495
                    }
496

            
497
6
                    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
498
4
                        tracing::debug!("Found v1 keyring: {name}");
499

            
500
                        // Try to load the keyring
501
10
                        match self.load_keyring(&path, name, secret.as_ref()).await {
502
2
                            Ok((label, alias, keyring)) => discovered.push((label, alias, keyring)),
503
                            Err(e) => tracing::warn!("Failed to load keyring {:?}: {}", path, e),
504
                        }
505
                    }
506
                }
507
            }
508
        }
509

            
510
        // Scan for v0 keyrings
511
4
        if keyrings_dir.exists() {
512
2
            tracing::debug!("Scanning for v0 keyrings in {}", keyrings_dir.display());
513
8
            if let Ok(mut entries) = tokio::fs::read_dir(&keyrings_dir).await {
514
10
                while let Ok(Some(entry)) = entries.next_entry().await {
515
2
                    let path = entry.path();
516

            
517
                    // Skip directories and non-.keyring files
518
4
                    if path.is_dir() || path.extension() != Some(std::ffi::OsStr::new("keyring")) {
519
                        continue;
520
                    }
521

            
522
6
                    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
523
4
                        tracing::debug!("Found v0 keyring: {name}");
524

            
525
                        // Try to load the keyring
526
10
                        match self.load_keyring(&path, name, secret.as_ref()).await {
527
2
                            Ok((label, alias, keyring)) => discovered.push((label, alias, keyring)),
528
2
                            Err(e) => tracing::warn!("Failed to load keyring {:?}: {}", path, e),
529
                        }
530
                    }
531
                }
532
            }
533
        }
534

            
535
4
        let pending_count = self.pending_migrations.lock().await.len();
536

            
537
4
        if discovered.is_empty() && pending_count == 0 {
538
2
            tracing::info!("No keyrings discovered in data directory");
539
        } else {
540
2
            tracing::info!(
541
                "Discovered {} keyring(s), {} pending v0 migration(s)",
542
                discovered.len(),
543
                pending_count
544
            );
545
        }
546

            
547
2
        Ok(discovered)
548
    }
549

            
550
    /// Load a single keyring from a file path
551
    /// Returns (label, alias, keyring)
552
2
    async fn load_keyring(
553
        &self,
554
        path: &std::path::Path,
555
        name: &str,
556
        secret: Option<&Secret>,
557
    ) -> Result<(String, String, Keyring), Error> {
558
4
        let alias = if name.eq_ignore_ascii_case(Self::LOGIN_ALIAS) {
559
4
            oo7::dbus::Service::DEFAULT_COLLECTION.to_owned()
560
        } else {
561
4
            name.to_owned().to_lowercase()
562
        };
563

            
564
        // Use name as label (capitalized for consistency with Login)
565
        let label = {
566
4
            let mut chars = name.chars();
567
2
            match chars.next() {
568
                None => String::new(),
569
2
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
570
            }
571
        };
572

            
573
        // Try to load the keyring
574
8
        let keyring = match LockedKeyring::load(path).await {
575
2
            Ok(locked_keyring) => {
576
                // Successfully loaded as v1 keyring
577
4
                if let Some(secret) = secret {
578
4
                    match locked_keyring.unlock(secret.clone()).await {
579
2
                        Ok(unlocked) => {
580
4
                            tracing::info!("Unlocked keyring '{}' from {:?}", name, path);
581
2
                            Keyring::Unlocked(unlocked)
582
                        }
583
2
                        Err(e) => {
584
4
                            tracing::warn!(
585
                                "Failed to unlock keyring '{}' with provided secret: {}. Keeping it locked.",
586
                                name,
587
                                e
588
                            );
589
                            // Reload as locked since unlock consumed it
590
6
                            Keyring::Locked(LockedKeyring::load(path).await?)
591
                        }
592
                    }
593
                } else {
594
4
                    tracing::debug!("No secret provided, keeping keyring '{}' locked", name);
595
2
                    Keyring::Locked(locked_keyring)
596
                }
597
            }
598
4
            Err(oo7::file::Error::VersionMismatch(Some(version)))
599
4
                if version.first() == Some(&0) =>
600
            // v0 is the legacy version
601
            {
602
                // This is a v0 keyring that needs migration
603
4
                tracing::info!(
604
                    "Found legacy v0 keyring '{name}' at {}, registering for migration",
605
                    path.display()
606
                );
607

            
608
4
                if let Some(secret) = secret {
609
4
                    tracing::debug!("Attempting immediate migration of v0 keyring '{name}'",);
610
6
                    match UnlockedKeyring::open(name, secret.clone()).await {
611
2
                        Ok(unlocked) => {
612
4
                            tracing::info!("Successfully migrated v0 keyring '{name}' to v1",);
613

            
614
                            // Write the migrated keyring to disk
615
6
                            unlocked.write().await?;
616
2
                            tracing::info!("Wrote migrated keyring '{name}' to disk");
617

            
618
                            // Remove the v0 keyring file after successful migration
619
6
                            if let Err(e) = tokio::fs::remove_file(path).await {
620
                                tracing::warn!(
621
                                    "Failed to remove v0 keyring at {}: {e}",
622
                                    path.display()
623
                                );
624
                            } else {
625
4
                                tracing::info!("Removed v0 keyring file at {}", path.display());
626
                            }
627

            
628
2
                            Keyring::Unlocked(unlocked)
629
                        }
630
2
                        Err(e) => {
631
4
                            tracing::warn!(
632
                                "Failed to migrate v0 keyring '{name}': {e}. Will retry when secret is available.",
633
                            );
634
6
                            self.pending_migrations.lock().await.insert(
635
4
                                name.to_owned(),
636
4
                                (path.to_path_buf(), label.clone(), alias.clone()),
637
                            );
638
2
                            return Err(e.into());
639
                        }
640
                    }
641
                } else {
642
4
                    tracing::debug!(
643
                        "No secret available for v0 keyring '{}', registering for pending migration",
644
                        name
645
                    );
646
6
                    self.pending_migrations.lock().await.insert(
647
4
                        name.to_owned(),
648
4
                        (path.to_path_buf(), label.clone(), alias.clone()),
649
                    );
650
2
                    return Err(Error::IO(std::io::Error::other(
651
                        "v0 keyring requires migration, no secret available",
652
                    )));
653
                }
654
            }
655
            Err(e) => {
656
                return Err(e.into());
657
            }
658
        };
659

            
660
2
        Ok((label, alias, keyring))
661
    }
662

            
663
    /// Initialize the service with collections and start client disconnect
664
    /// handler
665
2
    pub(crate) async fn initialize(
666
        &self,
667
        connection: zbus::Connection,
668
        mut discovered_keyrings: Vec<(String, String, Keyring)>, // (name, alias, keyring)
669
        auto_create_default: bool,
670
    ) -> Result<(), Error> {
671
8
        self.connection.set(connection.clone()).unwrap();
672

            
673
2
        let object_server = connection.object_server();
674
8
        let mut collections = self.collections.lock().await;
675

            
676
        // Check if we have a default collection
677
16
        let has_default = discovered_keyrings.iter().any(|(_, alias, _)| {
678
6
            alias == oo7::dbus::Service::DEFAULT_COLLECTION || alias == Self::LOGIN_ALIAS
679
        });
680

            
681
2
        if !has_default && auto_create_default {
682
            tracing::info!("No default collection found, creating 'Login' keyring");
683

            
684
            let locked_keyring = LockedKeyring::open(Self::LOGIN_ALIAS)
685
                .await
686
                .inspect_err(|e| {
687
                    tracing::error!("Failed to create default Login keyring: {}", e);
688
                })?;
689

            
690
            discovered_keyrings.push((
691
                "Login".to_owned(),
692
                oo7::dbus::Service::DEFAULT_COLLECTION.to_owned(),
693
                Keyring::Locked(locked_keyring),
694
            ));
695

            
696
            tracing::info!("Created default 'Login' collection (locked)");
697
        }
698

            
699
        // Set up discovered collections
700
16
        for (label, alias, keyring) in discovered_keyrings {
701
15
            let collection = Collection::new(&label, &alias, self.clone(), keyring).await;
702
4
            collections.insert(collection.path().to_owned().into(), collection.clone());
703
12
            collection.dispatch_items().await?;
704
17
            object_server
705
4
                .at(collection.path(), collection.clone())
706
16
                .await?;
707

            
708
            // If this is the default collection, also register it at the alias path
709
6
            if alias == oo7::dbus::Service::DEFAULT_COLLECTION {
710
30
                object_server
711
12
                    .at(DEFAULT_COLLECTION_ALIAS_PATH, collection)
712
24
                    .await?;
713
            }
714
        }
715

            
716
        // Always create session collection (always temporary)
717
        let collection = Collection::new(
718
            "session",
719
5
            oo7::dbus::Service::SESSION_COLLECTION,
720
8
            self.clone(),
721
14
            Keyring::Unlocked(UnlockedKeyring::temporary(Secret::random().unwrap()).await?),
722
        )
723
14
        .await;
724
11
        object_server
725
8
            .at(collection.path(), collection.clone())
726
15
            .await?;
727
4
        collections.insert(collection.path().to_owned().into(), collection);
728

            
729
4
        drop(collections); // Release the lock
730

            
731
        // Spawn client disconnect handler
732
4
        let service = self.clone();
733
12
        tokio::spawn(async move { service.on_client_disconnect().await });
734

            
735
6
        Ok(())
736
    }
737

            
738
12
    async fn on_client_disconnect(&self) -> zbus::Result<()> {
739
15
        let rule = zbus::MatchRule::builder()
740
4
            .msg_type(zbus::message::Type::Signal)
741
            .sender("org.freedesktop.DBus")?
742
            .interface("org.freedesktop.DBus")?
743
            .member("NameOwnerChanged")?
744
            .arg(2, "")?
745
            .build();
746
7
        let mut stream = zbus::MessageStream::for_match_rule(rule, self.connection(), None).await?;
747
12
        while let Some(message) = stream.try_next().await? {
748
            let body = message.body();
749
            let Ok((_name, old_owner, new_owner)) =
750
                body.deserialize::<(String, Optional<UniqueName<'_>>, Optional<UniqueName<'_>>)>()
751
            else {
752
                continue;
753
            };
754
            debug_assert!(new_owner.is_none()); // We enforce that in the matching rule
755
            let old_owner = old_owner
756
                .as_ref()
757
                .expect("A disconnected client requires an old_owner");
758
            if let Some(session) = self.session_from_sender(old_owner).await {
759
                match session.close().await {
760
                    Ok(_) => tracing::info!(
761
                        "Client {} disconnected. Session: {} closed.",
762
                        old_owner,
763
                        session.path()
764
                    ),
765
                    Err(err) => tracing::error!("Failed to close session: {}", err),
766
                }
767
            }
768
        }
769
        Ok(())
770
    }
771

            
772
2
    pub async fn set_locked(
773
        &self,
774
        locked: bool,
775
        objects: &[OwnedObjectPath],
776
    ) -> Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>), ServiceError> {
777
2
        let mut without_prompt = Vec::new();
778
2
        let mut with_prompt = Vec::new();
779
4
        let collections = self.collections.lock().await;
780

            
781
8
        for object in objects {
782
6
            for (path, collection) in collections.iter() {
783
4
                let collection_locked = collection.is_locked().await;
784
2
                if *object == *path {
785
2
                    if collection_locked == locked {
786
4
                        tracing::debug!(
787
                            "Collection: {} is already {}.",
788
                            object,
789
                            if locked { "locked" } else { "unlocked" }
790
                        );
791
4
                        without_prompt.push(object.clone());
792
2
                    } else if locked {
793
                        // Locking never requires a prompt
794
6
                        collection.set_locked(true, None).await?;
795
2
                        without_prompt.push(object.clone());
796
                    } else {
797
                        // Unlocking may require a prompt
798
4
                        with_prompt.push(object.clone());
799
                    }
800
                    break;
801
6
                } else if let Some(item) = collection.item_from_path(object).await {
802
6
                    if locked == item.is_locked().await {
803
2
                        tracing::debug!(
804
                            "Item: {} is already {}.",
805
                            object,
806
                            if locked { "locked" } else { "unlocked" }
807
                        );
808
4
                        without_prompt.push(object.clone());
809
                    // If the collection is unlocked, we can lock/unlock the
810
                    // item directly
811
2
                    } else if !collection_locked {
812
6
                        let keyring = collection.keyring.read().await;
813
8
                        item.set_locked(locked, keyring.as_ref().unwrap().as_unlocked())
814
8
                            .await?;
815
2
                        without_prompt.push(object.clone());
816
                    } else {
817
                        // Collection is locked, unlocking the item requires unlocking the
818
                        // collection
819
4
                        with_prompt.push(object.clone());
820
                    }
821
                    break;
822
                }
823
4
                tracing::warn!("Object: {} does not exist.", object);
824
            }
825
        }
826

            
827
2
        Ok((without_prompt, with_prompt))
828
    }
829

            
830
4
    pub fn connection(&self) -> &zbus::Connection {
831
4
        self.connection.get().unwrap()
832
    }
833

            
834
4
    pub fn object_server(&self) -> &zbus::ObjectServer {
835
4
        self.connection().object_server()
836
    }
837

            
838
8
    pub async fn collection_from_path(&self, path: &ObjectPath<'_>) -> Option<Collection> {
839
4
        let collections = self.collections.lock().await;
840
4
        collections.get(path).cloned()
841
    }
842

            
843
16
    pub async fn session_index(&self) -> u32 {
844
9
        let n_sessions = *self.session_index.read().await + 1;
845
5
        *self.session_index.write().await = n_sessions;
846

            
847
4
        n_sessions
848
    }
849

            
850
    async fn session_from_sender(&self, sender: &UniqueName<'_>) -> Option<Session> {
851
        let sessions = self.sessions.lock().await;
852

            
853
        sessions.values().find(|s| s.sender() == sender).cloned()
854
    }
855

            
856
8
    pub async fn session(&self, path: &ObjectPath<'_>) -> Option<Session> {
857
4
        self.sessions.lock().await.get(path).cloned()
858
    }
859

            
860
8
    pub async fn remove_session(&self, path: &ObjectPath<'_>) {
861
4
        self.sessions.lock().await.remove(path);
862
    }
863

            
864
8
    pub async fn remove_collection(&self, path: &ObjectPath<'_>) {
865
4
        self.collections.lock().await.remove(path);
866

            
867
2
        if let Ok(signal_emitter) =
868
            self.signal_emitter(oo7::dbus::api::Service::PATH.as_deref().unwrap())
869
        {
870
4
            let _ = self.collections_changed(&signal_emitter).await;
871
        }
872
    }
873

            
874
8
    pub async fn prompt_index(&self) -> u32 {
875
4
        let n_prompts = *self.prompt_index.read().await + 1;
876
2
        *self.prompt_index.write().await = n_prompts;
877

            
878
2
        n_prompts
879
    }
880

            
881
8
    pub async fn prompt(&self, path: &ObjectPath<'_>) -> Option<Prompt> {
882
4
        self.prompts.lock().await.get(path).cloned()
883
    }
884

            
885
8
    pub async fn remove_prompt(&self, path: &ObjectPath<'_>) {
886
4
        self.prompts.lock().await.remove(path);
887
        // Also clean up pending collection if it exists
888
2
        self.pending_collections.lock().await.remove(path);
889
    }
890

            
891
8
    pub async fn register_prompt(&self, path: OwnedObjectPath, prompt: Prompt) {
892
4
        self.prompts.lock().await.insert(path, prompt);
893
    }
894

            
895
2
    pub async fn pending_collection(
896
        &self,
897
        prompt_path: &ObjectPath<'_>,
898
    ) -> Option<(String, String)> {
899
8
        self.pending_collections
900
            .lock()
901
6
            .await
902
2
            .get(prompt_path)
903
            .cloned()
904
    }
905

            
906
2
    pub async fn complete_collection_creation(
907
        &self,
908
        prompt_path: &ObjectPath<'_>,
909
        secret: Secret,
910
    ) -> Result<OwnedObjectPath, ServiceError> {
911
        // Retrieve the pending collection metadata
912
4
        let Some((label, alias)) = self.pending_collection(prompt_path).await else {
913
4
            return Err(ServiceError::NoSuchObject(format!(
914
                "No pending collection for prompt `{prompt_path}`"
915
            )));
916
        };
917

            
918
        // Create a persistent keyring with the provided secret
919
12
        let keyring = UnlockedKeyring::open(&label.to_lowercase(), secret)
920
6
            .await
921
4
            .map_err(|err| custom_service_error(&format!("Failed to create keyring: {err}")))?;
922

            
923
        // Write the keyring file to disk immediately
924
8
        keyring
925
            .write()
926
8
            .await
927
2
            .map_err(|err| custom_service_error(&format!("Failed to write keyring file: {err}")))?;
928

            
929
2
        let keyring = Keyring::Unlocked(keyring);
930

            
931
        // Create the collection
932
6
        let collection = Collection::new(&label, &alias, self.clone(), keyring).await;
933
4
        let collection_path: OwnedObjectPath = collection.path().to_owned().into();
934

            
935
        // Register with object server
936
8
        self.object_server()
937
2
            .at(collection.path(), collection.clone())
938
6
            .await?;
939

            
940
        // Add to collections
941
8
        self.collections
942
            .lock()
943
6
            .await
944
2
            .insert(collection_path.clone(), collection);
945

            
946
        // Clean up pending collection
947
2
        self.pending_collections.lock().await.remove(prompt_path);
948

            
949
        // Emit CollectionCreated signal
950
2
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
951
2
        let signal_emitter = self.signal_emitter(service_path)?;
952
4
        Service::collection_created(&signal_emitter, &collection_path).await?;
953

            
954
        // Emit PropertiesChanged for Collections property to invalidate client cache
955
2
        self.collections_changed(&signal_emitter).await?;
956

            
957
2
        tracing::info!(
958
            "Collection `{}` created with label '{}'",
959
            collection_path,
960
            label
961
        );
962

            
963
2
        Ok(collection_path)
964
    }
965

            
966
10
    pub fn signal_emitter<'a, P>(
967
        &self,
968
        path: P,
969
    ) -> Result<zbus::object_server::SignalEmitter<'a>, oo7::dbus::ServiceError>
970
    where
971
        P: TryInto<ObjectPath<'a>>,
972
        P::Error: Into<zbus::Error>,
973
    {
974
20
        let signal_emitter = zbus::object_server::SignalEmitter::new(self.connection(), path)?;
975

            
976
10
        Ok(signal_emitter)
977
    }
978

            
979
    /// Extract the collection label from a list of object paths
980
    /// The objects can be either collections or items
981
8
    async fn extract_label_from_objects(&self, objects: &[OwnedObjectPath]) -> String {
982
4
        if objects.is_empty() {
983
            return String::new();
984
        }
985

            
986
        // Check if at least one of the objects is a Collection
987
8
        for object in objects {
988
6
            if let Some(collection) = self.collection_from_path(object).await {
989
4
                return collection.label().await;
990
            }
991
        }
992

            
993
        // Get the collection path from the first item
994
        // assumes all items are from the same collection
995
6
        if let Some(path_str) = objects.first().and_then(|p| p.as_str().rsplit_once('/')) {
996
2
            let collection_path = path_str.0;
997
6
            if let Ok(obj_path) = ObjectPath::try_from(collection_path) {
998
4
                if let Some(collection) = self.collection_from_path(&obj_path).await {
999
4
                    return collection.label().await;
                }
            }
        }
        String::new()
    }
    /// Extract the collection from a list of object paths
    /// The objects can be either collections or items
2
    async fn extract_collection_from_objects(
        &self,
        objects: &[OwnedObjectPath],
    ) -> Option<Collection> {
4
        if objects.is_empty() {
            return None;
        }
        // Check if at least one of the objects is a Collection
8
        for object in objects {
6
            if let Some(collection) = self.collection_from_path(object).await {
2
                return Some(collection);
            }
        }
        // Get the collection path from the first item
        // (assumes all items are from the same collection)
6
        let path = objects
            .first()
            .unwrap()
            .as_str()
            .rsplit_once('/')
4
            .map(|(parent, _)| parent)?;
6
        self.collection_from_path(&ObjectPath::try_from(path).unwrap())
4
            .await
    }
    /// Attempt to migrate pending v0 keyrings with the provided secret
    /// Returns a list of successfully migrated keyring names
18
    pub async fn migrate_pending_keyrings(&self, secret: &Secret) -> Vec<String> {
2
        let mut migrated = Vec::new();
4
        let mut pending = self.pending_migrations.lock().await;
2
        let mut to_remove = Vec::new();
8
        for (name, (path, label, alias)) in pending.iter() {
4
            tracing::debug!("Attempting to migrate pending v0 keyring: {}", name);
8
            match UnlockedKeyring::open(name, secret.clone()).await {
2
                Ok(unlocked) => {
4
                    tracing::info!("Successfully migrated v0 keyring '{}' to v1", name);
                    // Write the migrated keyring to disk
8
                    match unlocked.write().await {
                        Ok(_) => {
2
                            tracing::info!("Wrote migrated keyring '{}' to disk", name);
                            // Remove the v0 keyring file after successful migration
8
                            if let Err(e) = tokio::fs::remove_file(path).await {
                                tracing::warn!("Failed to remove v0 keyring at {:?}: {}", path, e);
                            } else {
4
                                tracing::info!("Removed v0 keyring file at {:?}", path);
                            }
                        }
                        Err(e) => {
                            tracing::error!(
                                "Failed to write migrated keyring '{}' to disk: {}",
                                name,
                                e
                            );
                            continue;
                        }
                    }
                    // Create a collection for this migrated keyring
2
                    let keyring = Keyring::Unlocked(unlocked);
8
                    let collection = Collection::new(label, alias, self.clone(), keyring).await;
2
                    let collection_path: OwnedObjectPath = collection.path().to_owned().into();
                    // Dispatch items
6
                    if let Err(e) = collection.dispatch_items().await {
                        tracing::error!(
                            "Failed to dispatch items for migrated keyring '{}': {}",
                            name,
                            e
                        );
                        continue;
                    }
8
                    if let Err(e) = self
                        .object_server()
2
                        .at(collection.path(), collection.clone())
8
                        .await
                    {
                        tracing::error!(
                            "Failed to register migrated collection '{}' with object server: {}",
                            name,
                            e
                        );
                        continue;
                    }
10
                    self.collections
                        .lock()
8
                        .await
4
                        .insert(collection_path.clone(), collection.clone());
2
                    if alias == oo7::dbus::Service::DEFAULT_COLLECTION {
                        if 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
                            );
                        }
                    }
4
                    if let Ok(signal_emitter) =
                        self.signal_emitter(oo7::dbus::api::Service::PATH.as_ref().unwrap())
                    {
4
                        let _ =
                            Service::collection_created(&signal_emitter, &collection_path).await;
6
                        let _ = self.collections_changed(&signal_emitter).await;
                    }
4
                    tracing::info!("Migrated keyring '{}' added as collection", name);
4
                    migrated.push(name.clone());
2
                    to_remove.push(name.clone());
                }
                Err(e) => {
                    tracing::debug!(
                        "Failed to migrate v0 keyring '{}' with provided secret: {}",
                        name,
                        e
                    );
                }
            }
        }
2
        for name in &to_remove {
4
            pending.remove(name);
        }
2
        migrated
    }
}
#[cfg(test)]
mod tests;