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
macro_rules! gnome_prompter_test {
18
    ($name:tt, $test_function:tt $(, $meta:meta)*) => {
19
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
20
110
        #[tokio::test]
21
108
        #[serial_test::serial(prompter_env)]
22
        $(
23
48
            #[$meta]
24
        )*
25
22
        async fn $name() -> Result<(), Box<dyn std::error::Error>> {
26
            unsafe {
27
22
                std::env::set_var("OO7_DAEMON_PROMPTER_TEST", "gnome");
28
            }
29
44
            let ret = $test_function().await;
30
88
            unsafe {
31
22
                std::env::remove_var("OO7_DAEMON_PROMPTER_TEST");
32
            }
33
66
            ret
34
        }
35
    }
36
}
37
pub(crate) use gnome_prompter_test;
38

            
39
macro_rules! plasma_prompter_test {
40
    ($name:tt, $test_function:tt $(, $meta:meta)*) => {
41
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
42

            
43
110
        #[tokio::test]
44
108
        #[serial_test::serial(prompter_env)]
45
        $(
46
48
            #[$meta]
47
        )*
48
22
        async fn $name() -> Result<(), Box<dyn std::error::Error>> {
49
            unsafe {
50
22
                std::env::set_var("OO7_DAEMON_PROMPTER_TEST", "plasma");
51
            }
52
44
            let ret = $test_function().await;
53
88
            unsafe {
54
22
                std::env::remove_var("OO7_DAEMON_PROMPTER_TEST");
55
            }
56
66
            ret
57
        }
58
    }
59
}
60
pub(crate) use plasma_prompter_test;
61

            
62
/// Helper to create a peer-to-peer connection pair using Unix socket
63
6
async fn create_p2p_connection()
64
-> Result<(zbus::Connection, zbus::Connection), Box<dyn std::error::Error>> {
65
6
    let guid = zbus::Guid::generate();
66
12
    let (p0, p1) = tokio::net::UnixStream::pair()?;
67

            
68
20
    let (client_conn, server_conn) = tokio::try_join!(
69
        // Client
70
12
        zbus::connection::Builder::unix_stream(p0).p2p().build(),
71
        // Server
72
12
        zbus::connection::Builder::unix_stream(p1)
73
6
            .server(guid)?
74
6
            .p2p()
75
6
            .build(),
76
    )?;
77

            
78
2
    Ok((server_conn, client_conn))
79
}
80

            
81
pub(crate) struct TestServiceSetup {
82
    pub server: Service,
83
    pub client_conn: zbus::Connection,
84
    pub service_api: dbus::api::Service,
85
    pub session: Arc<dbus::api::Session>,
86
    pub collections: Vec<dbus::api::Collection>,
87
    pub server_public_key: Option<oo7::Key>,
88
    pub keyring_secret: Option<oo7::Secret>,
89
    pub aes_key: Option<Arc<oo7::Key>>,
90
    #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
91
    pub mock_prompter: MockPrompterService,
92
    #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
93
    pub mock_prompter_plasma: MockPrompterServicePlasma,
94
}
95

            
96
impl TestServiceSetup {
97
    /// Get the default/Login collection
98
2
    pub(crate) async fn default_collection(
99
        &self,
100
    ) -> Result<&dbus::api::Collection, Box<dyn std::error::Error>> {
101
8
        for collection in &self.collections {
102
8
            let label = collection.label().await?;
103
4
            if label == "Login" {
104
2
                return Ok(collection);
105
            }
106
        }
107
        Err("Default collection not found".into())
108
    }
109

            
110
4
    pub(crate) async fn plain_session(
111
        with_default_collection: bool,
112
    ) -> Result<TestServiceSetup, Box<dyn std::error::Error>> {
113
10
        let (server_conn, client_conn) = create_p2p_connection().await?;
114

            
115
8
        let secret = if with_default_collection {
116
4
            Some(Secret::from("test-password-long-enough"))
117
        } else {
118
2
            None
119
        };
120

            
121
8
        let server = Service::run_with_connection(server_conn.clone(), secret.clone()).await?;
122

            
123
        // Create and serve the mock prompter
124
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
125
        let mock_prompter = {
126
3
            let mock_prompter = MockPrompterService::new();
127
12
            client_conn
128
                .object_server()
129
3
                .at("/org/gnome/keyring/Prompter", mock_prompter.clone())
130
11
                .await?;
131
5
            mock_prompter
132
        };
133
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
134
        let mock_prompter_plasma = {
135
4
            let mock_prompter_plasma = MockPrompterServicePlasma::new();
136
11
            client_conn
137
                .object_server()
138
4
                .at("/SecretPrompter", mock_prompter_plasma.clone())
139
14
                .await?;
140
4
            mock_prompter_plasma
141
        };
142

            
143
        // Give the server a moment to fully initialize
144
8
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
145

            
146
3
        let service_api = dbus::api::Service::new(&client_conn).await?;
147

            
148
10
        let (server_public_key, session) = service_api.open_session(None).await?;
149
9
        let session = Arc::new(session);
150

            
151
12
        let collections = service_api.collections().await?;
152

            
153
3
        Ok(TestServiceSetup {
154
3
            server,
155
4
            keyring_secret: secret,
156
4
            client_conn,
157
2
            service_api,
158
3
            session,
159
            collections,
160
3
            server_public_key,
161
            aes_key: None,
162
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
163
3
            mock_prompter,
164
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
165
3
            mock_prompter_plasma,
166
        })
167
    }
168

            
169
2
    pub(crate) async fn encrypted_session(
170
        with_default_collection: bool,
171
    ) -> Result<TestServiceSetup, Box<dyn std::error::Error>> {
172
6
        let (server_conn, client_conn) = create_p2p_connection().await?;
173

            
174
6
        let secret = if with_default_collection {
175
4
            Some(Secret::from("test-password-long-enough"))
176
        } else {
177
2
            None
178
        };
179

            
180
6
        let server = Service::run_with_connection(server_conn.clone(), secret.clone()).await?;
181

            
182
        // Create and serve the mock prompter
183
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
184
        let mock_prompter = {
185
2
            let mock_prompter = MockPrompterService::new();
186
8
            client_conn
187
                .object_server()
188
2
                .at("/org/gnome/keyring/Prompter", mock_prompter.clone())
189
6
                .await?;
190
2
            mock_prompter
191
        };
192

            
193
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
194
        let mock_prompter_plasma = {
195
2
            let mock_prompter_plasma = MockPrompterServicePlasma::new();
196
8
            client_conn
197
                .object_server()
198
2
                .at("/SecretPrompter", mock_prompter_plasma.clone())
199
6
                .await?;
200
2
            mock_prompter_plasma
201
        };
202

            
203
        // Give the server a moment to fully initialize
204
6
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
205

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

            
208
        // Generate client key pair for encrypted session
209
4
        let client_private_key = oo7::Key::generate_private_key()?;
210
4
        let client_public_key = oo7::Key::generate_public_key(&client_private_key)?;
211

            
212
6
        let (server_public_key, session) =
213
            service_api.open_session(Some(client_public_key)).await?;
214
4
        let session = Arc::new(session);
215

            
216
4
        let aes_key =
217
            oo7::Key::generate_aes_key(&client_private_key, &server_public_key.as_ref().unwrap())?;
218

            
219
6
        let collections = service_api.collections().await?;
220

            
221
2
        Ok(Self {
222
2
            server,
223
2
            keyring_secret: secret,
224
2
            client_conn,
225
2
            service_api,
226
2
            session,
227
2
            collections,
228
2
            server_public_key,
229
2
            aes_key: Some(Arc::new(aes_key)),
230
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
231
2
            mock_prompter,
232
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
233
2
            mock_prompter_plasma,
234
        })
235
    }
236

            
237
    /// Create a test setup that discovers keyrings from disk
238
    /// This is useful for PAM tests that need to create keyrings on disk first
239
2
    pub(crate) async fn with_disk_keyrings(
240
        secret: Option<Secret>,
241
    ) -> Result<TestServiceSetup, Box<dyn std::error::Error>> {
242
        use zbus::proxy::Defaults;
243

            
244
6
        let (server_conn, client_conn) = create_p2p_connection().await?;
245

            
246
2
        let service = crate::Service::default();
247

            
248
8
        server_conn
249
            .object_server()
250
            .at(
251
2
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
252
2
                service.clone(),
253
            )
254
6
            .await?;
255

            
256
6
        let discovered = service.discover_keyrings(secret.clone()).await?;
257
5
        service.initialize(server_conn, discovered, false).await?;
258

            
259
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
260
        let mock_prompter = {
261
2
            let mock_prompter = MockPrompterService::new();
262
8
            client_conn
263
                .object_server()
264
2
                .at("/org/gnome/keyring/Prompter", mock_prompter.clone())
265
6
                .await?;
266
2
            mock_prompter
267
        };
268

            
269
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
270
        let mock_prompter_plasma = {
271
2
            let mock_prompter_plasma = MockPrompterServicePlasma::new();
272
8
            client_conn
273
                .object_server()
274
2
                .at("/SecretPrompter", mock_prompter_plasma.clone())
275
6
                .await?;
276
2
            mock_prompter_plasma
277
        };
278

            
279
6
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
280

            
281
2
        let service_api = dbus::api::Service::new(&client_conn).await?;
282

            
283
6
        let (server_public_key, session) = service_api.open_session(None).await?;
284
4
        let session = Arc::new(session);
285

            
286
6
        let collections = service_api.collections().await?;
287

            
288
2
        Ok(TestServiceSetup {
289
2
            server: service,
290
2
            keyring_secret: secret,
291
2
            client_conn,
292
2
            service_api,
293
2
            session,
294
            collections,
295
2
            server_public_key,
296
            aes_key: None,
297
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
298
2
            mock_prompter,
299
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
300
2
            mock_prompter_plasma,
301
        })
302
    }
303

            
304
8
    pub(crate) async fn set_password_accept(&self, accept: bool) {
305
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
306
4
        self.mock_prompter.set_accept(accept).await;
307
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
308
2
        self.mock_prompter_plasma.set_accept(accept).await;
309
    }
310

            
311
8
    pub(crate) async fn set_password_queue(&self, passwords: Vec<oo7::Secret>) {
312
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
313
4
        self.mock_prompter
314
4
            .set_password_queue(passwords.clone())
315
4
            .await;
316
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
317
4
        self.mock_prompter_plasma
318
2
            .set_password_queue(passwords)
319
4
            .await;
320
    }
321
}
322

            
323
/// Mock implementation of org.gnome.keyring.internal.Prompter
324
///
325
/// This simulates the GNOME System Prompter for testing without requiring
326
/// the actual GNOME keyring prompter service to be running.
327
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
328
#[derive(Debug, Clone)]
329
pub(crate) struct MockPrompterService {
330
    /// The password to use for unlock prompts (simulates user input)
331
    unlock_password: Arc<tokio::sync::Mutex<Option<oo7::Secret>>>,
332
    /// Whether to accept (true) or dismiss (false) prompts
333
    should_accept: Arc<tokio::sync::Mutex<bool>>,
334
    /// Queue of passwords to use for for testing retry logic
335
    password_queue: Arc<tokio::sync::Mutex<Vec<oo7::Secret>>>,
336
}
337

            
338
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
339
impl MockPrompterService {
340
3
    pub fn new() -> Self {
341
        Self {
342
5
            unlock_password: Arc::new(tokio::sync::Mutex::new(Some(oo7::Secret::from(
343
                "test-password-long-enough",
344
            )))),
345
8
            should_accept: Arc::new(tokio::sync::Mutex::new(true)),
346
8
            password_queue: Arc::new(tokio::sync::Mutex::new(Vec::new())),
347
        }
348
    }
349

            
350
    /// Set whether prompts should be accepted or dismissed
351
8
    pub async fn set_accept(&self, accept: bool) {
352
4
        *self.should_accept.lock().await = accept;
353
    }
354

            
355
8
    pub async fn set_password_queue(&self, passwords: Vec<oo7::Secret>) {
356
2
        *self.password_queue.lock().await = passwords;
357
    }
358
}
359

            
360
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
361
#[zbus::interface(name = "org.gnome.keyring.internal.Prompter")]
362
impl MockPrompterService {
363
    async fn begin_prompting(
364
        &self,
365
        callback: ObjectPath<'_>,
366
        #[zbus(connection)] connection: &zbus::Connection,
367
    ) -> zbus::fdo::Result<()> {
368
        tracing::debug!("MockPrompter: begin_prompting called for {}", callback);
369
        let callback_path = callback.to_owned();
370
        let connection = connection.clone();
371

            
372
        // Spawn a task to send the initial prompt_ready call
373
        tokio::spawn(async move {
374
            tracing::debug!("MockPrompter: spawned task starting");
375
            // Small delay to ensure callback is fully registered
376
            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
377

            
378
            // Call PromptReady directly without building a proxy (avoids introspection
379
            // issues in p2p)
380
            tracing::debug!(
381
                "MockPrompter: calling PromptReady with None on {}",
382
                callback_path
383
            );
384
            let properties: HashMap<String, Value> = HashMap::new();
385
            let empty_exchange = "";
386

            
387
            connection
388
                .call_method(
389
                    None::<()>, // No destination in p2p
390
                    &callback_path,
391
                    Some("org.gnome.keyring.internal.Prompter.Callback"),
392
                    "PromptReady",
393
                    &(Optional::<Reply>::from(None), properties, empty_exchange),
394
                )
395
                .await?;
396

            
397
            tracing::debug!("MockPrompter: PromptReady(None) completed");
398
            Ok::<_, zbus::Error>(())
399
        });
400

            
401
        Ok(())
402
    }
403

            
404
    async fn perform_prompt(
405
        &self,
406
        callback: ObjectPath<'_>,
407
        type_: PromptType,
408
        _properties: Properties,
409
        exchange: &str,
410
        #[zbus(connection)] connection: &zbus::Connection,
411
    ) -> zbus::fdo::Result<()> {
412
        tracing::debug!(
413
            "MockPrompter: perform_prompt called for {}, type={:?}",
414
            callback,
415
            type_
416
        );
417
        // This is called by PrompterCallback.prompter_init() with the server's exchange
418
        let callback_path = callback.to_owned();
419
        let unlock_password = self.unlock_password.clone();
420
        let should_accept = self.should_accept.clone();
421
        let password_queue = self.password_queue.clone();
422
        let exchange = exchange.to_owned();
423
        let connection = connection.clone();
424

            
425
        // Spawn a task to simulate user interaction and send final response
426
        tokio::spawn(async move {
427
            tracing::debug!("MockPrompter: perform_prompt task starting");
428
            // Small delay to simulate user interaction
429
            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
430

            
431
            let accept = *should_accept.lock().await;
432
            let properties: HashMap<String, Value> = HashMap::new();
433

            
434
            if !accept {
435
                tracing::debug!("MockPrompter: dismissing prompt");
436
                // Dismiss the prompt
437
                connection
438
                    .call_method(
439
                        None::<()>, // No destination in p2p
440
                        &callback_path,
441
                        Some("org.gnome.keyring.internal.Prompter.Callback"),
442
                        "PromptReady",
443
                        &(Reply::No, properties, ""),
444
                    )
445
                    .await?;
446
                tracing::debug!("MockPrompter: PromptReady(no) completed");
447

            
448
                return Ok(());
449
            } else if type_ == PromptType::Password {
450
                tracing::debug!("MockPrompter: performing unlock (password prompt)");
451
                // Unlock prompt - perform secret exchange
452

            
453
                let mut queue = password_queue.lock().await;
454
                let password = if !queue.is_empty() {
455
                    let pwd = queue.remove(0);
456
                    tracing::debug!(
457
                        "MockPrompter: using password from queue (length: {}, queue remaining: {})",
458
                        std::str::from_utf8(pwd.as_bytes()).unwrap_or("<binary>"),
459
                        queue.len()
460
                    );
461
                    pwd
462
                } else {
463
                    let pwd = unlock_password.lock().await.clone().unwrap();
464
                    tracing::debug!(
465
                        "MockPrompter: using default password (length: {})",
466
                        std::str::from_utf8(pwd.as_bytes()).unwrap_or("<binary>")
467
                    );
468
                    pwd
469
                };
470
                drop(queue);
471

            
472
                // Generate our own key pair
473
                let private_key = oo7::Key::generate_private_key().unwrap();
474
                let public_key = crate::gnome::crypto::generate_public_key(&private_key).unwrap();
475

            
476
                // Handshake with server's exchange to get AES key
477
                let aes_key = secret_exchange::handshake(&private_key, &exchange).unwrap();
478

            
479
                // Encrypt the password
480
                let iv = crypto::generate_iv().unwrap();
481
                let encrypted = crypto::encrypt(password.as_bytes(), &aes_key, &iv).unwrap();
482

            
483
                // Create final exchange with encrypted secret
484
                let final_exchange = format!(
485
                    "[sx-aes-1]\npublic={}\nsecret={}\niv={}",
486
                    base64::prelude::BASE64_STANDARD.encode(public_key.as_ref()),
487
                    base64::prelude::BASE64_STANDARD.encode(&encrypted),
488
                    base64::prelude::BASE64_STANDARD.encode(&iv)
489
                );
490

            
491
                tracing::debug!("MockPrompter: calling PromptReady with yes");
492
                connection
493
                    .call_method(
494
                        None::<()>, // No destination in p2p
495
                        &callback_path,
496
                        Some("org.gnome.keyring.internal.Prompter.Callback"),
497
                        "PromptReady",
498
                        &(Reply::Yes, properties, final_exchange.as_str()),
499
                    )
500
                    .await?;
501
                tracing::debug!("MockPrompter: PromptReady(yes) with secret exchange completed");
502
            } else {
503
                tracing::debug!("MockPrompter: accepting confirm prompt");
504
                // Lock/confirm prompt - just accept
505
                connection
506
                    .call_method(
507
                        None::<()>, // No destination in p2p
508
                        &callback_path,
509
                        Some("org.gnome.keyring.internal.Prompter.Callback"),
510
                        "PromptReady",
511
                        &(Reply::Yes, properties, ""),
512
                    )
513
                    .await?;
514
                tracing::debug!("MockPrompter: PromptReady(yes) completed");
515
            }
516

            
517
            Ok::<_, zbus::Error>(())
518
        });
519

            
520
        Ok(())
521
    }
522

            
523
    async fn stop_prompting(
524
        &self,
525
        callback: ObjectPath<'_>,
526
        #[zbus(connection)] connection: &zbus::Connection,
527
    ) -> zbus::fdo::Result<()> {
528
        tracing::debug!("MockPrompter: stop_prompting called for {}", callback);
529
        let callback_path = callback.to_owned();
530
        let connection = connection.clone();
531

            
532
        tokio::spawn(async move {
533
            tracing::debug!("MockPrompter: calling PromptDone for {}", callback_path);
534
            let result = connection
535
                .call_method(
536
                    None::<()>,
537
                    &callback_path,
538
                    Some("org.gnome.keyring.internal.Prompter.Callback"),
539
                    "PromptDone",
540
                    &(),
541
                )
542
                .await;
543

            
544
            if let Err(err) = result {
545
                tracing::debug!("MockPrompter: PromptDone failed: {}", err);
546
            } else {
547
                tracing::debug!("MockPrompter: PromptDone completed for {}", callback_path);
548
            }
549
        });
550

            
551
        Ok(())
552
    }
553
}
554

            
555
/// Mock implementation of org.kde.secretprompter
556
///
557
/// This simulates the Plasma System Prompter for testing without requiring
558
/// the actual service to be running.
559
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
560
#[derive(Debug, Clone)]
561
pub(crate) struct MockPrompterServicePlasma {
562
    /// The password to use for unlock prompts (simulates user input)
563
    unlock_password: Arc<tokio::sync::Mutex<Option<oo7::Secret>>>,
564
    /// Whether to accept (true) or dismiss (false) prompts
565
    should_accept: Arc<tokio::sync::Mutex<bool>>,
566
    /// Queue of passwords to use for for testing retry logic
567
    password_queue: Arc<tokio::sync::Mutex<Vec<oo7::Secret>>>,
568
}
569

            
570
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
571

            
572
impl MockPrompterServicePlasma {
573
5
    pub fn new() -> Self {
574
        Self {
575
4
            unlock_password: Arc::new(tokio::sync::Mutex::new(Some(oo7::Secret::from(
576
                "test-password-long-enough",
577
            )))),
578
9
            should_accept: Arc::new(tokio::sync::Mutex::new(true)),
579
7
            password_queue: Arc::new(tokio::sync::Mutex::new(Vec::new())),
580
        }
581
    }
582

            
583
    /// Set whether prompts should be accepted or dismissed
584
8
    pub async fn set_accept(&self, accept: bool) {
585
4
        *self.should_accept.lock().await = accept;
586
    }
587

            
588
8
    pub async fn set_password_queue(&self, passwords: Vec<oo7::Secret>) {
589
2
        *self.password_queue.lock().await = passwords;
590
    }
591

            
592
2
    pub async fn send_secret(
593
        connection: &zbus::Connection,
594
        callback_path: &ObjectPath<'_>,
595
        secret: &oo7::Secret,
596
    ) -> zbus::fdo::Result<()> {
597
4
        let callback_path = callback_path.to_owned();
598
4
        let connection = connection.clone();
599
2
        let secret = secret.clone();
600

            
601
        // Accepted case
602
6
        tokio::spawn(async move {
603
4
            tracing::debug!(
604
                "MockPrompterServicePlasma: calling Accepted on {}",
605
                callback_path
606
            );
607

            
608
4
            let (read_fd, write_fd) = socketpair(
609
                AddressFamily::UNIX,
610
                SocketType::STREAM,
611
2
                SocketFlags::CLOEXEC | SocketFlags::NONBLOCK,
612
                None,
613
            )
614
2
            .expect("Failed to create socketpair");
615
4
            let mut file = File::from(write_fd);
616
4
            file.write_all(secret.as_bytes()).unwrap();
617
2
            drop(file); // Close write end to signal EOF
618

            
619
8
            connection
620
2
                .call_method(
621
                    None::<()>, // No destination in p2p
622
2
                    &callback_path,
623
                    Some("org.kde.secretprompter.request"),
624
                    "Accepted",
625
2
                    &(Fd::Owned(read_fd)),
626
                )
627
8
                .await?;
628

            
629
2
            tracing::debug!(
630
                "MockPrompterServicePlasma: Accepted completed for {}",
631
                callback_path
632
            );
633
2
            Ok::<_, zbus::Error>(())
634
        });
635

            
636
2
        Ok(())
637
    }
638
}
639

            
640
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
641
#[zbus::interface(name = "org.kde.secretprompter")]
642
impl MockPrompterServicePlasma {
643
2
    async fn unlock_collection_prompt(
644
        &self,
645
        request: ObjectPath<'_>,
646
        _window_id: &str,
647
        _activation_token: &str,
648
        _collection_name: &str,
649
        #[zbus(connection)] connection: &zbus::Connection,
650
    ) -> zbus::fdo::Result<()> {
651
4
        tracing::debug!(
652
            "MockPrompterServicePlasma: unlock_collection_prompt called for {}",
653
            request
654
        );
655

            
656
4
        let callback_path = request.to_owned();
657
4
        let connection = connection.clone();
658

            
659
        // Reject case
660
4
        if *self.should_accept.lock().await == false {
661
6
            tokio::spawn(async move {
662
4
                tracing::debug!(
663
                    "MockPrompterServicePlasma: dismissing prompt for {}",
664
                    callback_path
665
                );
666

            
667
10
                connection
668
2
                    .call_method(
669
                        None::<()>, // No destination in p2p
670
                        &callback_path,
671
                        Some("org.kde.secretprompter.request"),
672
                        "Rejected",
673
                        &(),
674
                    )
675
8
                    .await
676
4
                    .unwrap();
677

            
678
2
                tracing::debug!(
679
                    "MockPrompterServicePlasma: Dismissed completed for {}",
680
                    callback_path
681
                );
682
            });
683
2
            return Ok(());
684
        }
685

            
686
4
        let mut queue = self.password_queue.lock().await.clone();
687
2
        self.password_queue.lock().await.clear();
688
2
        if !queue.is_empty() {
689
12
            tokio::spawn(async move {
690
12
                let proxy: zbus::proxy::Proxy<'_> = zbus::proxy::Builder::new(&connection)
691
2
                    .destination("org.kde.client") // apparently unused but still required for p2p
692
2
                    .unwrap()
693
4
                    .path(callback_path.clone())
694
2
                    .unwrap()
695
2
                    .interface("org.kde.secretprompter.request")
696
2
                    .unwrap()
697
2
                    .build()
698
6
                    .await
699
2
                    .unwrap();
700
6
                let mut signal_stream = proxy.receive_signal("Retry").await.unwrap();
701

            
702
                loop {
703
2
                    let secret = queue.remove(0);
704
8
                    MockPrompterServicePlasma::send_secret(&connection, &callback_path, &secret)
705
8
                        .await
706
2
                        .unwrap();
707

            
708
2
                    if queue.is_empty() {
709
                        break;
710
                    }
711

            
712
                    // Wait for Retry signal before sending next secret from the queue
713
10
                    signal_stream.next().await;
714
                }
715
            });
716
        } else {
717
4
            let pwd = self.unlock_password.lock().await.clone().unwrap();
718
1
            tracing::debug!(
719
                "MockPrompterServicePlasma: using default password (length: {})",
720
                std::str::from_utf8(pwd.as_bytes()).unwrap_or("<binary>")
721
            );
722
4
            MockPrompterServicePlasma::send_secret(&connection, &callback_path, &pwd).await?;
723
        };
724

            
725
2
        Ok(())
726
    }
727

            
728
2
    async fn create_collection_prompt(
729
        &self,
730
        request: ObjectPath<'_>,
731
        window_id: &str,
732
        activation_token: &str,
733
        collection_name: &str,
734
        #[zbus(connection)] connection: &zbus::Connection,
735
    ) -> zbus::fdo::Result<()> {
736
4
        tracing::debug!(
737
            "MockPrompterServicePlasma: create_collection_prompt called for {}",
738
            request
739
        );
740
        // Behavior is identical for both prompts. Visualization would be different.
741
8
        self.unlock_collection_prompt(
742
2
            request,
743
            window_id,
744
            activation_token,
745
            collection_name,
746
            connection,
747
        )
748
6
        .await?;
749
2
        Ok(())
750
    }
751
}