1
use std::{collections::HashMap, fs::File, io::Write, sync::Arc};
2

            
3
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
4
use base64::Engine;
5
use oo7::{Secret, crypto, dbus};
6
use rustix::net::{AddressFamily, SocketFlags, SocketType, socketpair};
7
use tokio_stream::StreamExt;
8
use zbus::zvariant::{Fd, ObjectPath, Optional, Value};
9

            
10
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
11
use crate::gnome::{
12
    prompter::{PromptType, Properties, Reply},
13
    secret_exchange,
14
};
15
use crate::service::Service;
16

            
17
/// Helper to create a peer-to-peer connection pair using Unix socket
18
32
async fn create_p2p_connection()
19
-> Result<(zbus::Connection, zbus::Connection), Box<dyn std::error::Error>> {
20
37
    let guid = zbus::Guid::generate();
21
75
    let (p0, p1) = tokio::net::UnixStream::pair()?;
22

            
23
146
    let (client_conn, server_conn) = tokio::try_join!(
24
        // Client
25
75
        zbus::connection::Builder::unix_stream(p0).p2p().build(),
26
        // Server
27
82
        zbus::connection::Builder::unix_stream(p1)
28
39
            .server(guid)?
29
39
            .p2p()
30
41
            .build(),
31
    )?;
32

            
33
30
    Ok((server_conn, client_conn))
34
}
35

            
36
pub struct TestServiceSetup {
37
    pub server: Service,
38
    pub client_conn: zbus::Connection,
39
    pub service_api: dbus::api::Service,
40
    pub session: Arc<dbus::api::Session>,
41
    pub collections: Vec<dbus::api::Collection>,
42
    pub server_public_key: Option<oo7::Key>,
43
    pub keyring_secret: Option<oo7::Secret>,
44
    pub aes_key: Option<Arc<oo7::Key>>,
45
    #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
46
    pub(crate) mock_prompter: MockPrompterService,
47
    #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
48
    pub(crate) mock_prompter_plasma: MockPrompterServicePlasma,
49
    // Keep temp dir alive for duration of test
50
    _temp_dir: tempfile::TempDir,
51
}
52

            
53
impl TestServiceSetup {
54
    /// Get the default/Login collection
55
4
    pub async fn default_collection(
56
        &self,
57
    ) -> Result<&dbus::api::Collection, Box<dyn std::error::Error>> {
58
16
        for collection in &self.collections {
59
16
            let label = collection.label().await?;
60
8
            if label == "Login" {
61
4
                return Ok(collection);
62
            }
63
        }
64
        Err("Default collection not found".into())
65
    }
66

            
67
25
    pub async fn plain_session(
68
        with_default_collection: bool,
69
    ) -> Result<TestServiceSetup, Box<dyn std::error::Error>> {
70
85
        let (server_conn, client_conn) = create_p2p_connection().await?;
71

            
72
64
        let secret = if with_default_collection {
73
54
            Some(Secret::from("test-password-long-enough"))
74
        } else {
75
6
            None
76
        };
77

            
78
64
        let temp_dir = tempfile::TempDir::new()?;
79
        let server = Service::run_with_connection(
80
61
            server_conn.clone(),
81
64
            temp_dir.path().to_path_buf(),
82
31
            None,
83
33
            secret.clone(),
84
        )
85
116
        .await?;
86

            
87
        // Create and serve the mock prompter
88
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
89
        let mock_prompter = {
90
32
            let mock_prompter = MockPrompterService::new();
91
121
            client_conn
92
                .object_server()
93
33
                .at("/org/gnome/keyring/Prompter", mock_prompter.clone())
94
80
                .await?;
95
28
            mock_prompter
96
        };
97
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
98
        let mock_prompter_plasma = {
99
31
            let mock_prompter_plasma = MockPrompterServicePlasma::new();
100
118
            client_conn
101
                .object_server()
102
30
                .at("/SecretPrompter", mock_prompter_plasma.clone())
103
87
                .await?;
104
27
            mock_prompter_plasma
105
        };
106

            
107
        // Give the server a moment to fully initialize
108
82
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
109

            
110
21
        let service_api = dbus::api::Service::new(&client_conn).await?;
111

            
112
68
        let (server_public_key, session) = service_api.open_session(None).await?;
113
59
        let session = Arc::new(session);
114

            
115
96
        let collections = service_api.collections().await?;
116

            
117
25
        Ok(TestServiceSetup {
118
27
            server,
119
26
            keyring_secret: secret,
120
27
            client_conn,
121
26
            service_api,
122
27
            session,
123
            collections,
124
24
            server_public_key,
125
            aes_key: None,
126
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
127
26
            mock_prompter,
128
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
129
24
            mock_prompter_plasma,
130
25
            _temp_dir: temp_dir,
131
        })
132
    }
133

            
134
10
    pub async fn encrypted_session(
135
        with_default_collection: bool,
136
    ) -> Result<TestServiceSetup, Box<dyn std::error::Error>> {
137
30
        let (server_conn, client_conn) = create_p2p_connection().await?;
138

            
139
24
        let secret = if with_default_collection {
140
20
            Some(Secret::from("test-password-long-enough"))
141
        } else {
142
4
            None
143
        };
144

            
145
20
        let temp_dir = tempfile::TempDir::new()?;
146
        let server = Service::run_with_connection(
147
20
            server_conn.clone(),
148
20
            temp_dir.path().to_path_buf(),
149
10
            None,
150
10
            secret.clone(),
151
        )
152
40
        .await?;
153

            
154
        // Create and serve the mock prompter
155
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
156
        let mock_prompter = {
157
10
            let mock_prompter = MockPrompterService::new();
158
40
            client_conn
159
                .object_server()
160
10
                .at("/org/gnome/keyring/Prompter", mock_prompter.clone())
161
30
                .await?;
162
10
            mock_prompter
163
        };
164

            
165
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
166
        let mock_prompter_plasma = {
167
10
            let mock_prompter_plasma = MockPrompterServicePlasma::new();
168
40
            client_conn
169
                .object_server()
170
10
                .at("/SecretPrompter", mock_prompter_plasma.clone())
171
30
                .await?;
172
10
            mock_prompter_plasma
173
        };
174

            
175
        // Give the server a moment to fully initialize
176
30
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
177

            
178
20
        let service_api = dbus::api::Service::new(&client_conn).await?;
179

            
180
        // Generate client key pair for encrypted session
181
20
        let client_private_key = oo7::Key::generate_private_key()?;
182
20
        let client_public_key = oo7::Key::generate_public_key(&client_private_key)?;
183

            
184
20
        let (server_public_key, session) =
185
            service_api.open_session(Some(client_public_key)).await?;
186
20
        let session = Arc::new(session);
187

            
188
20
        let aes_key =
189
            oo7::Key::generate_aes_key(&client_private_key, server_public_key.as_ref().unwrap())?;
190

            
191
30
        let collections = service_api.collections().await?;
192

            
193
10
        Ok(Self {
194
10
            server,
195
10
            keyring_secret: secret,
196
10
            client_conn,
197
10
            service_api,
198
10
            session,
199
10
            collections,
200
10
            server_public_key,
201
10
            aes_key: Some(Arc::new(aes_key)),
202
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
203
10
            mock_prompter,
204
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
205
10
            mock_prompter_plasma,
206
10
            _temp_dir: temp_dir,
207
        })
208
    }
209

            
210
    /// Create a test setup that discovers keyrings from disk
211
    /// This is useful for PAM tests that need to create keyrings on disk first
212
4
    pub(crate) async fn with_disk_keyrings(
213
        data_dir: std::path::PathBuf,
214
        pam_socket: Option<std::path::PathBuf>,
215
        secret: Option<Secret>,
216
    ) -> Result<TestServiceSetup, Box<dyn std::error::Error>> {
217
        use zbus::proxy::Defaults;
218

            
219
12
        let (server_conn, client_conn) = create_p2p_connection().await?;
220

            
221
8
        let temp_dir = tempfile::TempDir::new()?;
222
4
        let service = crate::Service::new(data_dir, pam_socket);
223

            
224
16
        server_conn
225
            .object_server()
226
            .at(
227
4
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
228
4
                service.clone(),
229
            )
230
12
            .await?;
231

            
232
12
        let discovered = service.discover_keyrings(secret.clone()).await?;
233
20
        service
234
8
            .initialize(server_conn, discovered, secret.clone(), false)
235
16
            .await?;
236

            
237
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
238
        let mock_prompter = {
239
4
            let mock_prompter = MockPrompterService::new();
240
16
            client_conn
241
                .object_server()
242
4
                .at("/org/gnome/keyring/Prompter", mock_prompter.clone())
243
12
                .await?;
244
4
            mock_prompter
245
        };
246

            
247
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
248
        let mock_prompter_plasma = {
249
4
            let mock_prompter_plasma = MockPrompterServicePlasma::new();
250
16
            client_conn
251
                .object_server()
252
4
                .at("/SecretPrompter", mock_prompter_plasma.clone())
253
12
                .await?;
254
4
            mock_prompter_plasma
255
        };
256

            
257
13
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
258

            
259
4
        let service_api = dbus::api::Service::new(&client_conn).await?;
260

            
261
15
        let (server_public_key, session) = service_api.open_session(None).await?;
262
10
        let session = Arc::new(session);
263

            
264
15
        let collections = service_api.collections().await?;
265

            
266
4
        Ok(TestServiceSetup {
267
5
            server: service,
268
5
            keyring_secret: secret,
269
5
            client_conn,
270
5
            service_api,
271
5
            session,
272
            collections,
273
4
            server_public_key,
274
            aes_key: None,
275
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
276
4
            mock_prompter,
277
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
278
4
            mock_prompter_plasma,
279
4
            _temp_dir: temp_dir,
280
        })
281
    }
282

            
283
16
    pub(crate) async fn set_password_accept(&self, accept: bool) {
284
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
285
8
        self.mock_prompter.set_accept(accept).await;
286
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
287
4
        self.mock_prompter_plasma.set_accept(accept).await;
288
    }
289

            
290
16
    pub(crate) async fn set_password_queue(&self, passwords: Vec<oo7::Secret>) {
291
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
292
8
        self.mock_prompter
293
8
            .set_password_queue(passwords.clone())
294
8
            .await;
295
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
296
9
        self.mock_prompter_plasma
297
5
            .set_password_queue(passwords)
298
8
            .await;
299
    }
300

            
301
    /// Helper to create a DBusSecret
302
    ///
303
    /// Automatically handles plain vs encrypted based on whether aes_key is
304
    /// set.
305
8
    pub(crate) fn create_dbus_secret(
306
        &self,
307
        secret: impl Into<Secret>,
308
    ) -> Result<dbus::api::DBusSecret, Box<dyn std::error::Error>> {
309
8
        let secret = secret.into();
310
8
        let dbus_secret = if let Some(ref aes_key) = self.aes_key {
311
16
            dbus::api::DBusSecret::new_encrypted(Arc::clone(&self.session), secret, aes_key)?
312
        } else {
313
16
            dbus::api::DBusSecret::new(Arc::clone(&self.session), secret)
314
        };
315
8
        Ok(dbus_secret)
316
    }
317

            
318
    /// Helper to create a test item in the default collection (index 0)
319
    ///
320
    /// Automatically handles plain vs encrypted sessions based on whether
321
    /// aes_key is set.
322
16
    pub(crate) async fn create_item(
323
        &self,
324
        label: &str,
325
        attributes: &impl oo7::AsAttributes,
326
        secret: impl Into<Secret>,
327
        replace: bool,
328
    ) -> Result<dbus::api::Item, Box<dyn std::error::Error>> {
329
32
        let dbus_secret = self.create_dbus_secret(secret)?;
330

            
331
66
        let item = self.collections[0]
332
17
            .create_item(label, attributes, &dbus_secret, replace, None)
333
66
            .await?;
334

            
335
16
        Ok(item)
336
    }
337

            
338
    /// Helper to lock a collection
339
    ///
340
    /// Gets the server-side collection and locks it with the keyring secret.
341
4
    pub(crate) async fn lock_collection(
342
        &self,
343
        collection: &dbus::api::Collection,
344
    ) -> Result<(), Box<dyn std::error::Error>> {
345
16
        let server_collection = self
346
            .server
347
8
            .collection_from_path(collection.inner().path())
348
12
            .await
349
            .expect("Collection should exist");
350
16
        server_collection
351
8
            .set_locked(true, self.keyring_secret.clone())
352
12
            .await?;
353
4
        Ok(())
354
    }
355

            
356
    /// Helper to unlock a collection
357
    ///
358
    /// Gets the server-side collection and unlocks it with the keyring secret.
359
4
    pub(crate) async fn unlock_collection(
360
        &self,
361
        collection: &dbus::api::Collection,
362
    ) -> Result<(), Box<dyn std::error::Error>> {
363
16
        let server_collection = self
364
            .server
365
8
            .collection_from_path(collection.inner().path())
366
12
            .await
367
            .expect("Collection should exist");
368
16
        server_collection
369
8
            .set_locked(false, self.keyring_secret.clone())
370
12
            .await?;
371
4
        Ok(())
372
    }
373

            
374
    /// Helper to lock an item
375
    ///
376
    /// Gets the server-side collection and item, then locks the item.
377
4
    pub(crate) async fn lock_item(
378
        &self,
379
        item: &dbus::api::Item,
380
    ) -> Result<(), Box<dyn std::error::Error>> {
381
16
        let collection = self
382
            .server
383
8
            .collection_from_path(self.collections[0].inner().path())
384
12
            .await
385
            .expect("Collection should exist");
386

            
387
8
        let keyring = collection.keyring.read().await;
388
8
        let unlocked_keyring = keyring.as_ref().unwrap().as_unlocked();
389

            
390
16
        let server_item = collection
391
4
            .item_from_path(item.inner().path())
392
12
            .await
393
            .unwrap();
394
8
        server_item.set_locked(true, unlocked_keyring).await?;
395
4
        Ok(())
396
    }
397
}
398

            
399
/// Mock implementation of org.gnome.keyring.internal.Prompter
400
///
401
/// This simulates the GNOME System Prompter for testing without requiring
402
/// the actual GNOME keyring prompter service to be running.
403
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
404
#[derive(Clone)]
405
pub(crate) struct MockPrompterService {
406
    /// The password to use for unlock prompts (simulates user input)
407
    unlock_password: Arc<tokio::sync::Mutex<Option<oo7::Secret>>>,
408
    /// Whether to accept (true) or dismiss (false) prompts
409
    should_accept: Arc<tokio::sync::Mutex<bool>>,
410
    /// Queue of passwords to use for for testing retry logic
411
    password_queue: Arc<tokio::sync::Mutex<Vec<oo7::Secret>>>,
412
}
413

            
414
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
415
impl MockPrompterService {
416
30
    pub fn new() -> Self {
417
        Self {
418
35
            unlock_password: Arc::new(tokio::sync::Mutex::new(Some(oo7::Secret::from(
419
                "test-password-long-enough",
420
            )))),
421
67
            should_accept: Arc::new(tokio::sync::Mutex::new(true)),
422
68
            password_queue: Arc::new(tokio::sync::Mutex::new(Vec::new())),
423
        }
424
    }
425

            
426
    /// Set whether prompts should be accepted or dismissed
427
16
    pub async fn set_accept(&self, accept: bool) {
428
8
        *self.should_accept.lock().await = accept;
429
    }
430

            
431
16
    pub async fn set_password_queue(&self, passwords: Vec<oo7::Secret>) {
432
5
        *self.password_queue.lock().await = passwords;
433
    }
434
}
435

            
436
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
437
#[zbus::interface(name = "org.gnome.keyring.internal.Prompter")]
438
impl MockPrompterService {
439
11
    async fn begin_prompting(
440
        &self,
441
        callback: ObjectPath<'_>,
442
        #[zbus(connection)] connection: &zbus::Connection,
443
    ) -> zbus::fdo::Result<()> {
444
28
        tracing::debug!("MockPrompter: begin_prompting called for {}", callback);
445
25
        let callback_path = callback.to_owned();
446
25
        let connection = connection.clone();
447

            
448
        // Spawn a task to send the initial prompt_ready call
449
48
        tokio::spawn(async move {
450
28
            tracing::debug!("MockPrompter: spawned task starting");
451
            // Small delay to ensure callback is fully registered
452
38
            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
453

            
454
            // Call PromptReady directly without building a proxy (avoids introspection
455
            // issues in p2p)
456
10
            tracing::debug!(
457
                "MockPrompter: calling PromptReady with None on {}",
458
                callback_path
459
            );
460
11
            let properties: HashMap<String, Value> = HashMap::new();
461
23
            let empty_exchange = "";
462

            
463
44
            connection
464
10
                .call_method(
465
                    None::<()>, // No destination in p2p
466
                    &callback_path,
467
                    Some("org.gnome.keyring.internal.Prompter.Callback"),
468
                    "PromptReady",
469
21
                    &(Optional::<Reply>::from(None), properties, empty_exchange),
470
                )
471
43
                .await?;
472

            
473
12
            tracing::debug!("MockPrompter: PromptReady(None) completed");
474
11
            Ok::<_, zbus::Error>(())
475
        });
476

            
477
13
        Ok(())
478
    }
479

            
480
13
    async fn perform_prompt(
481
        &self,
482
        callback: ObjectPath<'_>,
483
        type_: PromptType,
484
        _properties: Properties,
485
        exchange: &str,
486
        #[zbus(connection)] connection: &zbus::Connection,
487
    ) -> zbus::fdo::Result<()> {
488
23
        tracing::debug!(
489
            "MockPrompter: perform_prompt called for {}, type={:?}",
490
            callback,
491
            type_
492
        );
493
        // This is called by GNOMEPrompterCallback.prompter_init() with the server's
494
        // exchange
495
24
        let callback_path = callback.to_owned();
496
24
        let unlock_password = self.unlock_password.clone();
497
24
        let should_accept = self.should_accept.clone();
498
24
        let password_queue = self.password_queue.clone();
499
24
        let exchange = exchange.to_owned();
500
24
        let connection = connection.clone();
501

            
502
        // Spawn a task to simulate user interaction and send final response
503
52
        tokio::spawn(async move {
504
23
            tracing::debug!("MockPrompter: perform_prompt task starting");
505
            // Small delay to simulate user interaction
506
38
            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
507

            
508
13
            let accept = *should_accept.lock().await;
509
14
            let properties: HashMap<String, Value> = HashMap::new();
510

            
511
13
            if !accept {
512
8
                tracing::debug!("MockPrompter: dismissing prompt");
513
                // Dismiss the prompt
514
16
                connection
515
4
                    .call_method(
516
                        None::<()>, // No destination in p2p
517
                        &callback_path,
518
                        Some("org.gnome.keyring.internal.Prompter.Callback"),
519
                        "PromptReady",
520
4
                        &(Reply::No, properties, ""),
521
                    )
522
16
                    .await?;
523
4
                tracing::debug!("MockPrompter: PromptReady(no) completed");
524

            
525
4
                return Ok(());
526
27
            } else if type_ == PromptType::Password {
527
27
                tracing::debug!("MockPrompter: performing unlock (password prompt)");
528
                // Unlock prompt - perform secret exchange
529

            
530
28
                let mut queue = password_queue.lock().await;
531
46
                let password = if !queue.is_empty() {
532
8
                    let pwd = queue.remove(0);
533
8
                    tracing::debug!(
534
                        "MockPrompter: using password from queue (length: {}, queue remaining: {})",
535
                        std::str::from_utf8(pwd.as_bytes()).unwrap_or("<binary>"),
536
                        queue.len()
537
                    );
538
4
                    pwd
539
                } else {
540
28
                    let pwd = unlock_password.lock().await.clone().unwrap();
541
14
                    tracing::debug!(
542
                        "MockPrompter: using default password (length: {})",
543
                        std::str::from_utf8(pwd.as_bytes()).unwrap_or("<binary>")
544
                    );
545
14
                    pwd
546
                };
547
14
                drop(queue);
548

            
549
                // Generate our own key pair
550
14
                let private_key = oo7::Key::generate_private_key().unwrap();
551
27
                let public_key = crate::gnome::crypto::generate_public_key(&private_key).unwrap();
552

            
553
                // Handshake with server's exchange to get AES key
554
29
                let aes_key = secret_exchange::handshake(&private_key, &exchange).unwrap();
555

            
556
                // Encrypt the password
557
29
                let iv = crypto::generate_iv().unwrap();
558
28
                let encrypted = crypto::encrypt(password.as_bytes(), &aes_key, &iv).unwrap();
559

            
560
                // Create final exchange with encrypted secret
561
16
                let final_exchange = format!(
562
                    "[sx-aes-1]\npublic={}\nsecret={}\niv={}",
563
32
                    base64::prelude::BASE64_STANDARD.encode(public_key.as_ref()),
564
16
                    base64::prelude::BASE64_STANDARD.encode(&encrypted),
565
16
                    base64::prelude::BASE64_STANDARD.encode(&iv)
566
                );
567

            
568
31
                tracing::debug!("MockPrompter: calling PromptReady with yes");
569
56
                connection
570
13
                    .call_method(
571
                        None::<()>, // No destination in p2p
572
                        &callback_path,
573
                        Some("org.gnome.keyring.internal.Prompter.Callback"),
574
                        "PromptReady",
575
15
                        &(Reply::Yes, properties, final_exchange.as_str()),
576
                    )
577
61
                    .await?;
578
13
                tracing::debug!("MockPrompter: PromptReady(yes) with secret exchange completed");
579
            } else {
580
                tracing::debug!("MockPrompter: accepting confirm prompt");
581
                // Lock/confirm prompt - just accept
582
                connection
583
                    .call_method(
584
                        None::<()>, // No destination in p2p
585
                        &callback_path,
586
                        Some("org.gnome.keyring.internal.Prompter.Callback"),
587
                        "PromptReady",
588
                        &(Reply::Yes, properties, ""),
589
                    )
590
                    .await?;
591
                tracing::debug!("MockPrompter: PromptReady(yes) completed");
592
            }
593

            
594
15
            Ok::<_, zbus::Error>(())
595
        });
596

            
597
13
        Ok(())
598
    }
599

            
600
12
    async fn stop_prompting(
601
        &self,
602
        callback: ObjectPath<'_>,
603
        #[zbus(connection)] connection: &zbus::Connection,
604
    ) -> zbus::fdo::Result<()> {
605
25
        tracing::debug!("MockPrompter: stop_prompting called for {}", callback);
606
25
        let callback_path = callback.to_owned();
607
25
        let connection = connection.clone();
608

            
609
40
        tokio::spawn(async move {
610
28
            tracing::debug!("MockPrompter: calling PromptDone for {}", callback_path);
611
39
            let result = connection
612
14
                .call_method(
613
                    None::<()>,
614
                    &callback_path,
615
                    Some("org.gnome.keyring.internal.Prompter.Callback"),
616
                    "PromptDone",
617
                    &(),
618
                )
619
51
                .await;
620

            
621
22
            if let Err(err) = result {
622
                tracing::debug!("MockPrompter: PromptDone failed: {}", err);
623
            } else {
624
22
                tracing::debug!("MockPrompter: PromptDone completed for {}", callback_path);
625
            }
626
        });
627

            
628
12
        Ok(())
629
    }
630
}
631

            
632
/// Mock implementation of org.kde.secretprompter
633
///
634
/// This simulates the Plasma System Prompter for testing without requiring
635
/// the actual service to be running.
636
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
637
#[derive(Clone)]
638
pub(crate) struct MockPrompterServicePlasma {
639
    /// The password to use for unlock prompts (simulates user input)
640
    unlock_password: Arc<tokio::sync::Mutex<Option<oo7::Secret>>>,
641
    /// Whether to accept (true) or dismiss (false) prompts
642
    should_accept: Arc<tokio::sync::Mutex<bool>>,
643
    /// Queue of passwords to use for for testing retry logic
644
    password_queue: Arc<tokio::sync::Mutex<Vec<oo7::Secret>>>,
645
}
646

            
647
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
648
impl MockPrompterServicePlasma {
649
31
    pub fn new() -> Self {
650
        Self {
651
34
            unlock_password: Arc::new(tokio::sync::Mutex::new(Some(oo7::Secret::from(
652
                "test-password-long-enough",
653
            )))),
654
65
            should_accept: Arc::new(tokio::sync::Mutex::new(true)),
655
65
            password_queue: Arc::new(tokio::sync::Mutex::new(Vec::new())),
656
        }
657
    }
658

            
659
    /// Set whether prompts should be accepted or dismissed
660
16
    pub async fn set_accept(&self, accept: bool) {
661
8
        *self.should_accept.lock().await = accept;
662
    }
663

            
664
17
    pub async fn set_password_queue(&self, passwords: Vec<oo7::Secret>) {
665
4
        *self.password_queue.lock().await = passwords;
666
    }
667

            
668
4
    pub async fn send_secret(
669
        connection: &zbus::Connection,
670
        callback_path: &ObjectPath<'_>,
671
        secret: &oo7::Secret,
672
    ) -> zbus::fdo::Result<()> {
673
8
        let callback_path = callback_path.to_owned();
674
8
        let connection = connection.clone();
675
4
        let secret = secret.clone();
676

            
677
        // Accepted case
678
12
        tokio::spawn(async move {
679
8
            tracing::debug!(
680
                "MockPrompterServicePlasma: calling Accepted on {}",
681
                callback_path
682
            );
683

            
684
8
            let (read_fd, write_fd) = socketpair(
685
                AddressFamily::UNIX,
686
                SocketType::STREAM,
687
4
                SocketFlags::CLOEXEC | SocketFlags::NONBLOCK,
688
                None,
689
            )
690
4
            .expect("Failed to create socketpair");
691
8
            let mut file = File::from(write_fd);
692
8
            file.write_all(secret.as_bytes()).unwrap();
693
4
            drop(file); // Close write end to signal EOF
694

            
695
16
            connection
696
4
                .call_method(
697
                    None::<()>, // No destination in p2p
698
4
                    &callback_path,
699
                    Some("org.kde.secretprompter.request"),
700
                    "Accepted",
701
4
                    &(Fd::Owned(read_fd)),
702
                )
703
16
                .await?;
704

            
705
4
            tracing::debug!(
706
                "MockPrompterServicePlasma: Accepted completed for {}",
707
                callback_path
708
            );
709
4
            Ok::<_, zbus::Error>(())
710
        });
711

            
712
4
        Ok(())
713
    }
714
}
715

            
716
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
717
#[zbus::interface(name = "org.kde.secretprompter")]
718
impl MockPrompterServicePlasma {
719
4
    async fn unlock_collection_prompt(
720
        &self,
721
        request: ObjectPath<'_>,
722
        _window_id: &str,
723
        _activation_token: &str,
724
        _collection_name: &str,
725
        #[zbus(connection)] connection: &zbus::Connection,
726
    ) -> zbus::fdo::Result<()> {
727
8
        tracing::debug!(
728
            "MockPrompterServicePlasma: unlock_collection_prompt called for {}",
729
            request
730
        );
731

            
732
8
        let callback_path = request.to_owned();
733
8
        let connection = connection.clone();
734

            
735
        // Reject case
736
8
        if !*self.should_accept.lock().await {
737
12
            tokio::spawn(async move {
738
8
                tracing::debug!(
739
                    "MockPrompterServicePlasma: dismissing prompt for {}",
740
                    callback_path
741
                );
742

            
743
20
                connection
744
4
                    .call_method(
745
                        None::<()>, // No destination in p2p
746
                        &callback_path,
747
                        Some("org.kde.secretprompter.request"),
748
                        "Rejected",
749
                        &(),
750
                    )
751
16
                    .await
752
8
                    .unwrap();
753

            
754
4
                tracing::debug!(
755
                    "MockPrompterServicePlasma: Dismissed completed for {}",
756
                    callback_path
757
                );
758
            });
759
4
            return Ok(());
760
        }
761

            
762
8
        let mut queue = self.password_queue.lock().await.clone();
763
4
        self.password_queue.lock().await.clear();
764
4
        if !queue.is_empty() {
765
24
            tokio::spawn(async move {
766
24
                let proxy: zbus::proxy::Proxy<'_> = zbus::proxy::Builder::new(&connection)
767
4
                    .destination("org.kde.client") // apparently unused but still required for p2p
768
4
                    .unwrap()
769
8
                    .path(callback_path.clone())
770
4
                    .unwrap()
771
4
                    .interface("org.kde.secretprompter.request")
772
4
                    .unwrap()
773
4
                    .build()
774
12
                    .await
775
4
                    .unwrap();
776
12
                let mut signal_stream = proxy.receive_signal("Retry").await.unwrap();
777

            
778
                loop {
779
4
                    let secret = queue.remove(0);
780
16
                    MockPrompterServicePlasma::send_secret(&connection, &callback_path, &secret)
781
16
                        .await
782
4
                        .unwrap();
783

            
784
4
                    if queue.is_empty() {
785
                        break;
786
                    }
787

            
788
                    // Wait for Retry signal before sending next secret from the queue
789
20
                    signal_stream.next().await;
790
                }
791
            });
792
        } else {
793
8
            let pwd = self.unlock_password.lock().await.clone().unwrap();
794
2
            tracing::debug!(
795
                "MockPrompterServicePlasma: using default password (length: {})",
796
                std::str::from_utf8(pwd.as_bytes()).unwrap_or("<binary>")
797
            );
798
8
            MockPrompterServicePlasma::send_secret(&connection, &callback_path, &pwd).await?;
799
        };
800

            
801
4
        Ok(())
802
    }
803

            
804
4
    async fn create_collection_prompt(
805
        &self,
806
        request: ObjectPath<'_>,
807
        window_id: &str,
808
        activation_token: &str,
809
        collection_name: &str,
810
        #[zbus(connection)] connection: &zbus::Connection,
811
    ) -> zbus::fdo::Result<()> {
812
8
        tracing::debug!(
813
            "MockPrompterServicePlasma: create_collection_prompt called for {}",
814
            request
815
        );
816
        // Behavior is identical for both prompts. Visualization would be different.
817
16
        self.unlock_collection_prompt(
818
4
            request,
819
            window_id,
820
            activation_token,
821
            collection_name,
822
            connection,
823
        )
824
12
        .await?;
825
4
        Ok(())
826
    }
827
}