Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add prompt implementation for service methods lock & unlock #173

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

warusadura
Copy link
Collaborator

@warusadura warusadura commented Dec 9, 2024

Please see the commits.

Copy link
Owner

@bilelmoussaoui bilelmoussaoui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also needs a rebase

client/src/crypto/native.rs Outdated Show resolved Hide resolved

let mut common_secret_bytes = common_secret.to_bytes_be();
let mut common_secret_padded = vec![0; 192 - common_secret_bytes.len()];
// inefficient, but ok for now
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is inefficient about it

client/src/crypto/native.rs Outdated Show resolved Hide resolved
client/src/crypto/openssl.rs Outdated Show resolved Hide resolved
client/src/crypto/openssl.rs Outdated Show resolved Hide resolved
server/src/secret_exchange.rs Outdated Show resolved Hide resolved
server/src/secret_exchange.rs Outdated Show resolved Hide resolved
server/src/secret_exchange.rs Outdated Show resolved Hide resolved
server/src/secret_exchange.rs Outdated Show resolved Hide resolved
let (unlocked, _not_unlocked) = self.set_locked(false, &objects).await?;
let (unlocked, not_unlocked) = self.set_locked(false, &objects).await?;
if !not_unlocked.is_empty() {
let prompt = Prompt::new(self.clone()).await;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt also needs a way to pass it a future that would be executed when the prompt is done

@warusadura
Copy link
Collaborator Author

Thanks for the prompt review @bilelmoussaoui

@@ -1,6 +1,6 @@
use std::{
ops::{Mul, Rem, Shr},
sync::LazyLock,
sync::{LazyLock, OnceLock},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lacks an openssl implementation as well as that is what most distros will likely use the daemon with

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, thanks!

Co-authored-by: Dhanuka Warusadura <[email protected]>

Signed-off-by: Dhanuka Warusadura <[email protected]>
@warusadura warusadura force-pushed the secret_exchange branch 3 times, most recently from db19706 to 49e0912 Compare January 10, 2025 08:25
@warusadura
Copy link
Collaborator Author

Sorry for the delay @bilelmoussaoui

I have removed the prompt implementation for now. Just wanted get the crypto part and secret_exchange impl cleared before moving into the Prompt impl. I have removed a lot of unwraps, but I'm still not sure this is any good. Very much like to hear what you think of the current impl :)

const CIPHER_TEXT_LEN: usize = 16;

#[derive(Debug, Zeroize)]
pub struct SecretExchange {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not sure how the SecretExchange is supposed to be used. The current API, even if used internally only, is not really nice. why there is a need for a SecretExchange struct at all? who would call begin/create_shared_secret? and what about retrieve_secret? I would need to see how it is used in a real-case scenario to provide any meaningful feedback

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thanks!

server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
server/src/gnome/prompter.rs Outdated Show resolved Hide resolved

impl PrompterCallback {
pub async fn new(service: Service) -> Self {
let index = service.prompt_index().await;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should create a PrompterProxy here and keepe it around for the functions below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. But I can't access self inside tokio::spawn. All these following functions are associated functions.

server/src/prompt.rs Outdated Show resolved Hide resolved
Comment on lines 117 to 141
let objects = prompt.objects().clone();
let result = Value::new(&objects).try_to_owned().unwrap();

tokio::spawn(async move {
let _ = service.set_locked(true, &objects, true).await;
});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing the objects, this should pass a future directly that you can spawn here. Keeping the logic that is supposed to be in Service::set_locked, stay there.

Comment on lines +444 to +466
pub async fn prompt(&self) -> Option<Prompt> {
let prompts = self.prompts.lock().await;

prompts.last().cloned()
}

pub async fn remove_prompt(&self) {
// prompts should always contain one item during a prompt related operation and
// it should be cleaned up afterwards.
self.prompts.lock().await.pop();
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there would always be one single prompt (which i don't believe is true, except for maybe GNOME specifically) as the API clearly allows modal keyring dialogs so every app could have an ongoing prompt operation.

@@ -318,6 +336,7 @@ impl Service {
&self,
locked: bool,
objects: &[OwnedObjectPath],
prompt: bool,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

Comment on lines 92 to 113
tokio::spawn(PrompterCallback::perform_prompt(
connection, path, "confirm", properties, exchange,
));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this has to be in PrompterCallback,

SecretExchange allows exchange of secrets between two processes on the
same system without exposing those secrets.

See https://gnome.pages.gitlab.gnome.org/gcr/gcr-4/class.SecretExchange.html

Signed-off-by: Dhanuka Warusadura <[email protected]>
@warusadura warusadura changed the title WIP: Add prompt implementation for service method unlock WIP: Add prompt implementation for service methods lock & unlock Jan 13, 2025
server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
match prompt.role() {
PromptRole::Lock => {
if properties.is_empty() {
// First PromptReady call
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't most of the prompting code be exactly the same no matter the PromptRole? the only thing that i see being different is the Properties and the future to execute once the prompt is done

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct. I have just pushed the unlock commit (wip) and the code looks mostly the same with a couple of changes.

server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
server/src/gnome/prompter.rs Outdated Show resolved Hide resolved
#[derive(Debug, DeserializeDict, SerializeDict, Type)]
#[zvariant(signature = "a{sv}")]
// System prompt properties
pub struct Properties {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this approach, during the second PromptReady call return I'm getting the following error, Unexpected non-0 padding byte 1. I can't figure out why. I think this is related to this. Then again all the fields here are properly renamed using #[zvariant(rename = "")

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check what gets send over the bus with busctl and see what is going on? Probably a mismatched type

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, thanks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried everything I can think of. I think this is happening due to a bug in zbus. Probably related to zbus issues #1193 and #320.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write a minimal reproducer of the issue? I am using the exact same thing in ashpd and it works fine.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm this is caused by the properties and not the the reply param of PromptReady?

Copy link
Collaborator Author

@warusadura warusadura Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed the notification.

Yes I'm 100% certain this is caused by Properties struct. Everything works with the HashMap<&str, zvariant::OwnedValue> approach.

git diff:

diff --git a/server/src/gnome/prompter.rs b/server/src/gnome/prompter.rs
index a139fba..b4104e5 100644
--- a/server/src/gnome/prompter.rs
+++ b/server/src/gnome/prompter.rs
@@ -1,11 +1,13 @@
 // org.gnome.keyring.Prompter
 // https://gitlab.gnome.org/GNOME/gcr/-/blob/main/gcr/org.gnome.keyring.Prompter.xml
 
+use std::collections::HashMap;
+
 use clap::error::Result;
 use oo7::dbus::ServiceError;
 use serde::{Deserialize, Serialize};
 use zbus::{
-    interface, proxy,
+    interface, proxy, zvariant,
     zvariant::{DeserializeDict, OwnedObjectPath, OwnedValue, SerializeDict, Type, Value},
 };
 
@@ -138,7 +140,7 @@ pub trait Prompter {
         &self,
         callback: OwnedObjectPath,
         type_: PromptType,
-        properties: Properties,
+        properties: HashMap<&str, OwnedValue>,
         exchange: &str,
     ) -> Result<(), ServiceError>;
 
@@ -157,7 +159,7 @@ impl PrompterCallback {
     pub async fn prompt_ready(
         &self,
         reply: Reply,
-        _properties: Properties,
+        _properties: HashMap<&str, OwnedValue>,
         exchange: &str,
         #[zbus(connection)] connection: &zbus::Connection,
     ) -> Result<(), ServiceError> {
@@ -181,6 +183,21 @@ impl PrompterCallback {
                         })?;
                         let exchange = secret_exchange.begin();
 
+                        let mut p: HashMap<&str, zvariant::OwnedValue> = HashMap::new();
+                        p.insert("continue-label", Value::new("Lock").try_to_owned().unwrap());
+                        p.insert(
+                            "description",
+                            Value::new("Confirm locking \"login\", Keyring.")
+                                .try_to_owned()
+                                .unwrap(),
+                        );
+                        p.insert(
+                            "message",
+                            Value::new("Lock Keyring").try_to_owned().unwrap(),
+                        );
+                        p.insert("caller-window", Value::new("").try_to_owned().unwrap());
+                        p.insert("cancel-label", Value::new("Cancel").try_to_owned().unwrap());
+
                         let properties = Properties::for_lock();
                         let path = self.path.clone();
 
@@ -188,7 +205,7 @@ impl PrompterCallback {
                             connection.clone(),
                             path,
                             PromptType::Confirm,
-                            properties,
+                            p,
                             exchange,
                         ));
                     }
@@ -261,6 +278,8 @@ impl PrompterCallback {
                                 })?;
                         self.service.set_secret_exchange_key(aes_key).await;
 
+                        let p: HashMap<&str, zvariant::OwnedValue> = HashMap::new();
+
                         let properties = Properties::for_unlock();
                         let path = self.path.clone();
 
@@ -268,7 +287,7 @@ impl PrompterCallback {
                             connection.clone(),
                             path,
                             PromptType::Confirm,
-                            properties,
+                            p,
                             daemon_exchange,
                         ));
                     }
@@ -373,7 +392,7 @@ impl PrompterCallback {
         connection: zbus::Connection,
         path: OwnedObjectPath,
         prompt_type: PromptType,
-        properties: Properties,
+        properties: HashMap<&str, OwnedValue>,
         exchange: String,
     ) -> Result<(), ServiceError> {
         let prompter = PrompterProxy::new(&connection).await?;

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then keep the Properties struct there and add a todo comment about it so we can investigate it further.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, thanks! I think I'll have to keep this diff as well. I can't test anything without it :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just locally.

description: Option<String>,
message: Option<String>,
#[zvariant(rename = "caller-window")]
caller_window: Option<String>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a WindowIdentifierType from ashpd

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know anything about ashpd. I'll look into it :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to do anything special here, just use that type.

pub enum Reply {
No,
Yes,
Empty,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does empty corresponds to? Please link in the code to where those answers are defined

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty corresponds to: ''. The first PromptReady call's reply argument is always '' and also the properties dict is empty.

image

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if you can use the zvariant::Optional type here and drop the Empty variant.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey. I tried reply: Optional<Reply> approach and I think the current approach with Empty variant is bit better or shorter. Because all we have to do is impl Serialize and Deserialize. When using Optional, I had to impl NoneValue and bunch of String, &str traits. I just stopped at: trait FromStr is not implemented for Reply. Should I keep Empty variant or continue with Optional?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use zvariant::Optional please. and you shouldn't have to implement that many traits but if you are struggling with it, you can leave it as is and i will take care of it before merging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks :)

Empty,
}

const NO: &str = "no";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for those consts

#[derive(Deserialize, Serialize, Debug, Type)]
#[serde(rename_all = "lowercase")]
#[zvariant(signature = "s")]
// Possible values for PerformPrompt type parameter
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this implement the serialisation traits or the derived ones are correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the derived ones are correct. I had to implement the serialisation and de-serialisation for Reply because of the Empty variant.

pub async fn prompt_ready(
&self,
reply: Reply,
_properties: Properties,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this unused? Already mentioned it before but you marked the thread as resolved except not really...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take another look.

#[zbus(connection)] connection: &zbus::Connection,
#[zbus(object_server)] object_server: &zbus::ObjectServer,
) -> Result<(), ServiceError> {
let callback = PrompterCallback::new(self.service.clone()).await;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt callback creates the prompt path again, just pass it here instead

@warusadura warusadura force-pushed the secret_exchange branch 2 times, most recently from 2399214 to 23067f3 Compare January 19, 2025 14:25
@bilelmoussaoui
Copy link
Owner

cc @ueno, could you review & ack the crypto code please?

@@ -47,6 +47,14 @@ impl Key {
Ok(Self::new(crypto::generate_public_key(private_key)?))
}

pub fn generate_public_key_for_secret_exchange(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those new methods needs tests, similar to the previous ones to ensure we have the same behaviour in both native & openssl crypto.

@@ -86,6 +86,16 @@ pub(crate) fn generate_public_key(private_key: impl AsRef<[u8]>) -> Result<Vec<u
Ok(public_key_uint.to_bytes_be())
}

pub(crate) fn generate_public_key_for_secret_exchange(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As those APIs are only useful for the secret exchange, which is only used by the GNOME implementation I don't think we should have them here and instead move them to the server side under /gnome. Especially that we have a Key::new

title: Some("Unlock Keyring".to_string()),
choice_label: None,
description: Some(
"An application wants access to the keyring 'login', but it is locked".to_string(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes the keyring used. Shouldn't be hardcoded but rather passed as a parameter

exchange,
&self.service.secret_exchange_key().await,
) {
match oo7::file::Keyring::open("login", secret).await {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, this assumes the keyring to open

) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
let (unlocked, _not_unlocked) = self.set_locked(false, &objects, false).await?;
let (unlocked, not_unlocked) = self.set_locked(false, &objects, false).await?;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as i mentioned earlier, Prompt should take a Future. The current code is way too complex just to avoid passing a future which would make everything much easier to follow

Even though gnome-keyring-daemon doesn't implement/show a prompt during
the service method lock. The secret service spec says a prompt can be
used during lock. So, we're adding this to the new daemon.

Signed-off-by: Dhanuka Warusadura <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants