From 4cd6484021815d51a5df3465db888be66ff8427d Mon Sep 17 00:00:00 2001 From: Dhanuka Warusadura Date: Tue, 28 Jan 2025 23:55:18 +0530 Subject: [PATCH 1/3] server: Use DH with the 1536 ike modp group for SecretExchange This change adds crypto implementations and key generation for SecretExchange. Co-authored-by: Daiki Ueno Signed-off-by: Dhanuka Warusadura --- Cargo.lock | 9 +++- server/Cargo.toml | 14 +++++ server/src/gnome/crypto/mod.rs | 9 ++++ server/src/gnome/crypto/native.rs | 86 ++++++++++++++++++++++++++++++ server/src/gnome/crypto/openssl.rs | 48 +++++++++++++++++ server/src/gnome/keys.rs | 70 ++++++++++++++++++++++++ server/src/gnome/mod.rs | 2 + server/src/main.rs | 1 + 8 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 server/src/gnome/crypto/mod.rs create mode 100644 server/src/gnome/crypto/native.rs create mode 100644 server/src/gnome/crypto/openssl.rs create mode 100644 server/src/gnome/keys.rs create mode 100644 server/src/gnome/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e4acf58a..7dd39f16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -553,7 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1213,9 +1213,14 @@ version = "0.3.0" dependencies = [ "clap", "enumflags2", + "hkdf", + "num", + "num-bigint-dig", "oo7", + "openssl", "rpassword", "serde", + "sha2", "tokio", "tracing", "tracing-subscriber", @@ -1677,7 +1682,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 6e97970c..8558d85b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,11 +11,25 @@ version.workspace = true [dependencies] clap.workspace = true enumflags2 = "0.7" +hkdf = { version = "0.12", optional = true } +num = "0.4.0" +num-bigint-dig = { version = "0.8", features = ["zeroize"] } +openssl = { version = "0.10", optional = true } oo7 = { workspace = true, features = ["unstable"] } rpassword = "7.3" serde.workspace = true +sha2 = { version = "0.10", optional = true } tokio = { workspace = true, features = ["full"] } tracing = "0.1" tracing-subscriber.workspace = true zbus.workspace = true zeroize.workspace = true + +[features] +default = ["native_crypto"] +native_crypto = [ + "dep:hkdf", + "dep:sha2", +] + +openssl_crypto = ["dep:openssl"] diff --git a/server/src/gnome/crypto/mod.rs b/server/src/gnome/crypto/mod.rs new file mode 100644 index 00000000..017d41f0 --- /dev/null +++ b/server/src/gnome/crypto/mod.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "native_crypto")] +mod native; +#[cfg(feature = "native_crypto")] +pub use native::*; + +#[cfg(feature = "openssl_crypto")] +mod openssl; +#[cfg(feature = "openssl_crypto")] +pub use self::openssl::*; diff --git a/server/src/gnome/crypto/native.rs b/server/src/gnome/crypto/native.rs new file mode 100644 index 00000000..6442b08b --- /dev/null +++ b/server/src/gnome/crypto/native.rs @@ -0,0 +1,86 @@ +use std::{ + ops::{Mul, Rem, Shr}, + sync::LazyLock, +}; + +use hkdf::Hkdf; +use num::{FromPrimitive, Integer, One, Zero}; +use num_bigint_dig::BigUint; +use oo7::crypto::Error; +use sha2::Sha256; +use zeroize::{Zeroize, Zeroizing}; + +pub fn generate_public_key_for_secret_exchange( + private_key: impl AsRef<[u8]>, +) -> Result, Error> { + let private_key_uint = BigUint::from_bytes_be(private_key.as_ref()); + static DH_GENERATOR: LazyLock = LazyLock::new(|| BigUint::from_u64(0x2).unwrap()); + let public_key_uint = powm_for_secret_exchange(&DH_GENERATOR, private_key_uint); + + Ok(public_key_uint.to_bytes_be()) +} + +pub fn generate_aes_key_for_secret_exchange( + private_key: impl AsRef<[u8]>, + server_public_key: impl AsRef<[u8]>, +) -> Result>, Error> { + let server_public_key_uint = BigUint::from_bytes_be(server_public_key.as_ref()); + let private_key_uint = BigUint::from_bytes_be(private_key.as_ref()); + let common_secret = powm_for_secret_exchange(&server_public_key_uint, private_key_uint); + + 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 + common_secret_padded.append(&mut common_secret_bytes); + + // hkdf + // input_keying_material + let ikm = common_secret_padded; + let salt = None; + let info = []; + + // output keying material + let mut okm = Zeroizing::new(vec![0; 16]); + + let (_, hk) = Hkdf::::extract(salt, &ikm); + hk.expand(&info, okm.as_mut()) + .expect("hkdf expand should never fail"); + + Ok(okm) +} + +fn powm_for_secret_exchange(base: &BigUint, mut exp: BigUint) -> BigUint { + // for key exchange + static DH_PRIME: LazyLock = LazyLock::new(|| { + BigUint::from_bytes_be(&[ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, + 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08, + 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, + 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, + 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, + 0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, + 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38, + 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, + 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE4, 0x5B, 0x3D, 0xC2, 0x00, 0x7C, 0xB8, 0xA1, 0x63, + 0xBF, 0x05, 0x98, 0xDA, 0x48, 0x36, 0x1C, 0x55, 0xD3, 0x9A, 0x69, 0x16, 0x3F, 0xA8, + 0xFD, 0x24, 0xCF, 0x5F, 0x83, 0x65, 0x5D, 0x23, 0xDC, 0xA3, 0xAD, 0x96, 0x1C, 0x62, + 0xF3, 0x56, 0x20, 0x85, 0x52, 0xBB, 0x9E, 0xD5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6D, + 0x67, 0x0C, 0x35, 0x4E, 0x4A, 0xBC, 0x98, 0x04, 0xF1, 0x74, 0x6C, 0x08, 0xCA, 0x23, + 0x73, 0x27, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ]) + }); + + let mut base = base.clone(); + let mut result: BigUint = One::one(); + + while !exp.is_zero() { + if exp.is_odd() { + result = result.mul(&base).rem(&*DH_PRIME); + } + exp = exp.shr(1); + base = (&base).mul(&base).rem(&*DH_PRIME); + } + exp.zeroize(); + + result +} diff --git a/server/src/gnome/crypto/openssl.rs b/server/src/gnome/crypto/openssl.rs new file mode 100644 index 00000000..8e93724d --- /dev/null +++ b/server/src/gnome/crypto/openssl.rs @@ -0,0 +1,48 @@ +use oo7::crypto::Error; +use openssl::{bn::BigNum, dh::Dh, md::Md, pkey::Id, pkey_ctx::PkeyCtx}; +use zeroize::Zeroizing; + +pub fn generate_public_key_for_secret_exchange( + private_key: impl AsRef<[u8]>, +) -> Result, Error> { + let private_key_bn = BigNum::from_slice(private_key.as_ref()).unwrap(); + let dh = Dh::from_pqg( + BigNum::get_rfc3526_prime_1536().unwrap(), + None, + BigNum::from_u32(2).unwrap(), + )?; + Ok(dh.set_private_key(private_key_bn)?.public_key().to_vec()) +} + +pub fn generate_aes_key_for_secret_exchange( + private_key: impl AsRef<[u8]>, + server_public_key: impl AsRef<[u8]>, +) -> Result>, Error> { + let private_key_bn = BigNum::from_slice(private_key.as_ref()).unwrap(); + let server_public_key_bn = BigNum::from_slice(server_public_key.as_ref()).unwrap(); + let dh = Dh::from_pqg( + BigNum::get_rfc2409_prime_1024().unwrap(), + None, + BigNum::from_u32(2).unwrap(), + )?; + let mut common_secret_bytes = dh + .set_private_key(private_key_bn)? + .compute_key(&server_public_key_bn)?; + + let mut common_secret_padded = vec![0; 192 - common_secret_bytes.len()]; + // inefficient, but ok for now + common_secret_padded.append(&mut common_secret_bytes); + + // hkdf + // input_keying_material + let ikm = common_secret_padded; + + let mut okm = Zeroizing::new(vec![0; 16]); + let mut ctx = PkeyCtx::new_id(Id::HKDF).unwrap(); + ctx.derive_init().unwrap(); + ctx.set_hkdf_md(Md::sha256()).unwrap(); + ctx.set_hkdf_key(&ikm).unwrap(); + ctx.derive(Some(okm.as_mut())) + .expect("hkdf expand should never fail"); + Ok(okm) +} diff --git a/server/src/gnome/keys.rs b/server/src/gnome/keys.rs new file mode 100644 index 00000000..f102c80c --- /dev/null +++ b/server/src/gnome/keys.rs @@ -0,0 +1,70 @@ +use oo7::Key; + +use crate::gnome::crypto; + +pub fn generate_public_key_for_secret_exchange( + private_key: &Key, +) -> Result { + Ok(Key::new(crypto::generate_public_key_for_secret_exchange( + private_key, + )?)) +} + +pub fn generate_aes_key_for_secret_exchange( + private_key: &Key, + server_public_key: &Key, +) -> Result { + Ok(Key::new( + crypto::generate_aes_key_for_secret_exchange(private_key, server_public_key)?.to_vec(), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn private_public_pair() { + let private_key = Key::new(vec![ + 191, 143, 254, 234, 148, 241, 169, 47, 207, 248, 155, 147, 31, 135, 9, 252, 203, 38, + 111, 153, 120, 220, 173, 131, 233, 139, 1, 243, 247, 232, 70, 77, 196, 244, 247, 67, + 232, 254, 100, 194, 155, 37, 248, 243, 70, 206, 170, 133, 192, 248, 253, 74, 74, 194, + 238, 165, 102, 235, 143, 235, 218, 48, 229, 250, 61, 149, 201, 144, 222, 3, 137, 67, + 253, 250, 90, 228, 209, 56, 237, 242, 250, 100, 156, 123, 104, 67, 175, 211, 113, 52, + 152, 107, 69, 13, 70, 30, 89, 146, 193, 149, 62, 100, 247, 37, 57, 44, 71, 49, 101, 29, + 80, 151, 240, 155, 160, 221, 64, 62, 189, 137, 218, 236, 24, 104, 230, 80, 245, 162, + ]); + let client_public_key = Key::new(vec![ + 90, 240, 105, 52, 245, 225, 112, 238, 63, 100, 6, 129, 215, 27, 199, 53, 210, 16, 60, + 140, 121, 222, 20, 224, 196, 152, 123, 154, 90, 212, 82, 183, 160, 222, 206, 83, 74, + 110, 121, 218, 76, 194, 34, 71, 194, 230, 224, 31, 24, 169, 58, 157, 181, 207, 104, + 182, 223, 223, 29, 254, 144, 74, 129, 204, 114, 165, 224, 109, 23, 123, 66, 139, 251, + 65, 57, 203, 15, 19, 210, 99, 128, 87, 198, 139, 175, 159, 219, 236, 206, 72, 219, 240, + 169, 220, 93, 148, 166, 176, 153, 89, 164, 13, 56, 145, 246, 167, 238, 221, 190, 117, + 148, 10, 211, 24, 19, 84, 204, 2, 111, 240, 32, 90, 252, 154, 81, 254, 230, 56, + ]); + + let expected_public_key = &[ + 64, 119, 1, 90, 244, 5, 65, 235, 2, 24, 179, 85, 248, 14, 227, 193, 177, 100, 179, 54, + 15, 189, 209, 52, 204, 221, 65, 61, 111, 212, 130, 34, 98, 117, 155, 4, 126, 155, 207, + 67, 101, 221, 64, 126, 4, 246, 123, 85, 242, 214, 123, 95, 182, 66, 154, 54, 30, 133, + 199, 185, 196, 60, 65, 36, 242, 251, 174, 218, 47, 220, 76, 131, 169, 11, 201, 124, 67, + 184, 200, 189, 181, 157, 180, 45, 120, 231, 51, 26, 26, 111, 236, 201, 42, 234, 122, + 58, 235, 3, 115, 245, 159, 243, 214, 146, 235, 13, 84, 45, 243, 162, 89, 23, 50, 70, + 139, 104, 64, 138, 214, 193, 244, 94, 177, 110, 165, 96, 238, 152, 146, 136, 10, 86, + 238, 189, 93, 69, 161, 20, 241, 132, 73, 249, 56, 24, 64, 54, 184, 1, 199, 94, 165, 44, + 113, 111, 81, 237, 4, 220, 44, 117, 18, 49, 229, 241, 248, 234, 195, 190, 65, 200, 233, + 150, 227, 57, 195, 103, 140, 237, 165, 145, 27, 68, 88, 166, 2, 46, 210, 212, 163, 185, + 115, 68, + ]; + let expected_aes_key = &[ + 246, 159, 99, 143, 217, 217, 155, 222, 215, 178, 246, 113, 252, 190, 156, 70, + ]; + + let public_key = generate_public_key_for_secret_exchange(&private_key); + let aes_key = generate_aes_key_for_secret_exchange(&private_key, &client_public_key); + + assert_eq!(public_key.unwrap().as_ref(), expected_public_key); + assert_eq!(aes_key.unwrap().as_ref(), expected_aes_key); + } +} diff --git a/server/src/gnome/mod.rs b/server/src/gnome/mod.rs new file mode 100644 index 00000000..bdd53dca --- /dev/null +++ b/server/src/gnome/mod.rs @@ -0,0 +1,2 @@ +pub mod crypto; +pub mod keys; diff --git a/server/src/main.rs b/server/src/main.rs index 26219e9e..83ca250e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,5 +1,6 @@ mod collection; mod error; +mod gnome; mod item; mod prompt; mod service; From 86a54e44a327155e258de7c916de026bf4849db3 Mon Sep 17 00:00:00 2001 From: Dhanuka Warusadura Date: Wed, 29 Jan 2025 00:02:25 +0530 Subject: [PATCH 2/3] server: Add SecretExchange implementation 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 --- Cargo.lock | 7 ++ server/Cargo.toml | 1 + server/src/gnome/mod.rs | 1 + server/src/gnome/secret_exchange.rs | 176 ++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 server/src/gnome/secret_exchange.rs diff --git a/Cargo.lock b/Cargo.lock index 7dd39f16..7099d44a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,6 +286,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.6.0" @@ -1211,6 +1217,7 @@ dependencies = [ name = "oo7-daemon" version = "0.3.0" dependencies = [ + "base64", "clap", "enumflags2", "hkdf", diff --git a/server/Cargo.toml b/server/Cargo.toml index 8558d85b..dd20f6bc 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +base64 = "0.22" clap.workspace = true enumflags2 = "0.7" hkdf = { version = "0.12", optional = true } diff --git a/server/src/gnome/mod.rs b/server/src/gnome/mod.rs index bdd53dca..912ef16b 100644 --- a/server/src/gnome/mod.rs +++ b/server/src/gnome/mod.rs @@ -1,2 +1,3 @@ pub mod crypto; pub mod keys; +pub mod secret_exchange; diff --git a/server/src/gnome/secret_exchange.rs b/server/src/gnome/secret_exchange.rs new file mode 100644 index 00000000..c78d74a9 --- /dev/null +++ b/server/src/gnome/secret_exchange.rs @@ -0,0 +1,176 @@ +// SecretExchange: Exchange secrets between processes in an unexposed way. + +// Initial C implementation: https://gitlab.gnome.org/GNOME/gcr/-/blob/master/gcr/gcr-secret-exchange.c + +// The initial implementation of SecretExchange/GCRSecretExchange uses a KeyFile +// to encode/parse the payload. In this implementation the payload is based +// on a HashMap. +// Before any transit operations the payload is base64 encoded and parsed into a +// String. + +use std::collections::HashMap; + +use base64::prelude::*; +use oo7::{crypto, Key}; +use zeroize::Zeroize; + +const SECRET: &str = "secret"; +const PUBLIC: &str = "public"; +const PRIVATE: &str = "private"; +const IV: &str = "iv"; +const PROTOCOL: &str = "[sx-aes-1]\n"; +const CIPHER_TEXT_LEN: usize = 16; + +#[derive(Debug, Zeroize)] +pub struct SecretExchange { + private_key: Key, + public_key: Key, +} + +impl SecretExchange { + // Creates the initial payload containing caller public_key + pub fn begin(&self) -> String { + let map = HashMap::from([(PUBLIC, self.public_key.as_ref())]); + + encode(&map) + } + + // Creates the shared secret: an AES key + pub fn create_shared_secret(&self, exchange: &str) -> Result { + let decoded = decode(exchange) + .expect("SecretExchange decode error: failed to decode exchange string"); + let server_public_key = Key::new( + decoded + .get(PUBLIC) + .expect("SecretExchange decode error: PUBLIC parameter is empty") + .to_vec(), + ); + // Above two calls should never fail during SecretExchange + let aes_key = crate::gnome::keys::generate_aes_key_for_secret_exchange( + &self.private_key, + &server_public_key, + )?; + let map = HashMap::from([(PRIVATE, aes_key.as_ref())]); + + Ok(encode(&map)) + } + + // Retrieves the secret from final secret exchange string + pub fn retrieve_secret(exchange: &str, aes_key: &str) -> Option { + let decoded = decode(exchange)?; + + // If we cancel an ongoing prompt call, the final exchange won't have the secret + // or IV. The following is to avoid `Option::unwrap()` on a `None` value + let secret = decoded.get(SECRET)?; + + if secret.len() != CIPHER_TEXT_LEN { + // To avoid a short secret/cipher-text causing an UnpadError during decryption + let false_secret = vec![0, 1]; + return Some(oo7::Secret::from(false_secret)); + } + + let iv = decoded.get(IV)?; + let decoded = decode(aes_key)?; + let aes_key = Key::new(decoded.get(PRIVATE)?.to_vec()); + + match crypto::decrypt(secret, &aes_key, iv) { + Ok(decrypted) => Some(oo7::Secret::from(decrypted)), + Err(err) => { + tracing::error!("Failed to do crypto decrypt: {}", err); + None + } + } + } + + pub fn new() -> Result { + let private_key = Key::generate_private_key()?; + let public_key = crate::gnome::keys::generate_public_key_for_secret_exchange(&private_key)?; + + Ok(Self { + private_key, + public_key, + }) + } +} + +// Converts a HashMap into a payload String +fn encode(map: &HashMap<&str, &[u8]>) -> String { + let mut exchange = map + .iter() + .map(|(key, value)| format!("{}={}", key, BASE64_STANDARD.encode(value))) + .collect::>() + .join("\n"); + exchange.insert_str(0, PROTOCOL); // Add PROTOCOL prefix + + exchange +} + +// Converts a payload String into a HashMap +fn decode(exchange: &str) -> Option>> { + let (_, exchange) = exchange.split_once(PROTOCOL)?; // Remove PROTOCOL prefix + let pairs = exchange.split("\n").collect::>(); + let mut map: HashMap<&str, Vec> = HashMap::new(); + + for pair in pairs { + if pair.is_empty() { + // To avoid splitting an empty line (last new line) + break; + } + let (key, value) = pair.split_once("=")?; + let encoded = BASE64_STANDARD.decode(value).unwrap_or(vec![]); + if encoded.is_empty() { + return None; + } + map.insert(key, encoded); + } + + Some(map) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_retrieve_secret() { + let exchange = "[sx-aes-1] +public=/V6FpknNXlOGJwPqXtN0RaED2bS5JyYbftv7WbD0gWiVTMoNgxkAuOX2g+zUO/4TdfBJ6viPRcNdYV+KcxskGvhYouFXs+IgKqNO0MF0CNnWra1I6G56SM4Bgstkx9M5J+1f83l/BTAxlLsAppeLkqEEVSQoy9jXhPOrl5XlIzF2DvriYh+FInB7SFz4VzE3KVq40p7tA9+iAVQg1o9qkQHLazFb1DfbWRgvhDVhwNkk1fIlepIeM426gdmHIAxP +secret=DBeLBvEgGuGygDm+XnkxyQ== +iv=8e3N+gx553PgQlfTKRK3JA=="; + + let aes_key = "[sx-aes-1] +private=zDWLKDent/C//LquHCTlGg=="; + + let decrypted = SecretExchange::retrieve_secret(exchange, aes_key).unwrap(); + assert_eq!(b"password".to_vec(), decrypted.to_vec()); + } + + #[test] + fn test_secret_exchange() { + let peer_1 = SecretExchange::new().unwrap(); + let peer_1_exchange = peer_1.begin(); + let peer_2 = SecretExchange::new().unwrap(); + let peer_2_exchange = peer_2.begin(); + let peer_1_aes_key = peer_1.create_shared_secret(&peer_2_exchange).unwrap(); + let peer_2_aes_key = peer_2.create_shared_secret(&peer_1_exchange).unwrap(); + + let decoded_pub = decode(&peer_2_exchange).unwrap(); + let pub_key = Key::new(decoded_pub.get(PUBLIC).unwrap().to_vec()); + + let decoded_aes = decode(&peer_2_aes_key).unwrap(); + let aes_key = Key::new(decoded_aes.get(PRIVATE).unwrap().to_vec()); + let iv = crypto::generate_iv().unwrap(); + let encrypted = crypto::encrypt(b"password".to_vec(), &aes_key, &iv).unwrap(); + + let map = HashMap::from([ + (PUBLIC, pub_key.as_ref()), + (SECRET, encrypted.as_ref()), + (IV, iv.as_ref()), + ]); + + let final_exchange = encode(&map); + + let decrypted = SecretExchange::retrieve_secret(&final_exchange, &peer_1_aes_key).unwrap(); + assert_eq!(b"password".to_vec(), decrypted.to_vec()); + } +} From 6f06a64ee8fd466e918b113ffdad6f9beafd5f53 Mon Sep 17 00:00:00 2001 From: Dhanuka Warusadura Date: Wed, 29 Jan 2025 18:39:47 +0530 Subject: [PATCH 3/3] Add prompt implementation for service methods lock and unlock 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 --- server/src/gnome/mod.rs | 1 + server/src/gnome/prompter.rs | 407 +++++++++++++++++++++++++++++++++++ server/src/prompt.rs | 86 +++++++- server/src/service.rs | 83 ++++++- 4 files changed, 564 insertions(+), 13 deletions(-) create mode 100644 server/src/gnome/prompter.rs diff --git a/server/src/gnome/mod.rs b/server/src/gnome/mod.rs index 912ef16b..54d37267 100644 --- a/server/src/gnome/mod.rs +++ b/server/src/gnome/mod.rs @@ -1,3 +1,4 @@ pub mod crypto; pub mod keys; +pub mod prompter; pub mod secret_exchange; diff --git a/server/src/gnome/prompter.rs b/server/src/gnome/prompter.rs new file mode 100644 index 00000000..1c937295 --- /dev/null +++ b/server/src/gnome/prompter.rs @@ -0,0 +1,407 @@ +// org.gnome.keyring.Prompter +// https://gitlab.gnome.org/GNOME/gcr/-/blob/main/gcr/org.gnome.keyring.Prompter.xml + +use clap::error::Result; +use oo7::dbus::ServiceError; +use serde::{Deserialize, Serialize}; +use zbus::{ + interface, proxy, + zvariant::{ + DeserializeDict, NoneValue, Optional, OwnedObjectPath, OwnedValue, SerializeDict, Type, + Value, + }, +}; + +use crate::{ + gnome::secret_exchange::SecretExchange, + prompt::{Prompt, PromptRole}, + service::Service, +}; + +#[derive(Debug, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] +// GcrPrompt properties: https://gitlab.gnome.org/GNOME/gcr/-/blob/main/gcr/gcr-prompt.c#L95 +// This struct causes "Unexpected non-0 padding byte `1`" error. +// Use properties: HashMap<&str, OwnedValue> to avoid this. +pub struct Properties { + title: Option, + message: Option, + description: Option, + warning: Option, + #[zvariant(rename = "password-new")] + password_new: Option, + #[zvariant(rename = "password-strength")] + password_strength: Option, + #[zvariant(rename = "choice-label")] + choice_label: Option, + #[zvariant(rename = "choice-chosen")] + choice_chosen: Option, + #[zvariant(rename = "caller-window")] + caller_window: Option, + #[zvariant(rename = "continue-label")] + continue_label: Option, + #[zvariant(rename = "cancel-label")] + cancel_label: Option, +} + +impl Properties { + fn for_lock(object: &str) -> Self { + Self { + title: None, + message: Some("Lock Keyring".to_owned()), + description: Some(format!("Confirm locking '{}' Keyring", object)), + warning: None, + password_new: None, + password_strength: None, + choice_label: None, + choice_chosen: None, + caller_window: Some("".to_owned()), + continue_label: Some("Lock".to_owned()), + cancel_label: Some("Cancel".to_owned()), + } + } + + fn for_unlock(object: &str, warning: &str) -> Self { + Self { + title: Some("Unlock Keyring".to_owned()), + message: Some("Authentication required".to_owned()), + description: Some(format!( + "An application wants access to the keyring '{}', but it is locked", + object + )), + warning: Some(warning.to_owned()), + password_new: None, + password_strength: None, + choice_label: None, + choice_chosen: None, + caller_window: Some("".to_owned()), + continue_label: Some("Unlock".to_owned()), + cancel_label: Some("Cancel".to_owned()), + } + } +} + +#[derive(Deserialize, Serialize, Debug, Type)] +#[serde(rename_all = "lowercase")] +#[zvariant(signature = "s")] +// Possible values for PromptReady reply parameter +pub enum Reply { + No, + Yes, +} + +impl NoneValue for Reply { + type NoneType = String; + + fn null_value() -> Self::NoneType { + String::new() + } +} + +impl TryFrom for Reply { + type Error = String; + + fn try_from(value: String) -> Result { + match value.as_str() { + "no" => Ok(Reply::No), + "yes" => Ok(Reply::Yes), + _ => Err("Invalid value".to_string()), + } + } +} + +#[derive(Deserialize, Serialize, Debug, Type)] +#[serde(rename_all = "lowercase")] +#[zvariant(signature = "s")] +// Possible values for PerformPrompt type parameter +pub enum PromptType { + Confirm, + Password, +} + +// org.gnome.keyring.internal.Prompter + +#[proxy( + default_service = "org.gnome.keyring.SystemPrompter", + interface = "org.gnome.keyring.internal.Prompter", + default_path = "/org/gnome/keyring/Prompter" +)] +pub trait Prompter { + fn begin_prompting(&self, callback: &OwnedObjectPath) -> Result<(), ServiceError>; + + fn perform_prompt( + &self, + callback: OwnedObjectPath, + type_: PromptType, + properties: Properties, + exchange: &str, + ) -> Result<(), ServiceError>; + + fn stop_prompting(&self, callback: OwnedObjectPath) -> Result<(), ServiceError>; +} + +// org.gnome.keyring.internal.Prompter.Callback + +pub struct PrompterCallback { + service: Service, + path: OwnedObjectPath, +} + +#[interface(name = "org.gnome.keyring.internal.Prompter.Callback")] +impl PrompterCallback { + pub async fn prompt_ready( + &self, + reply: Optional, + _properties: Properties, + exchange: &str, + #[zbus(connection)] connection: &zbus::Connection, + ) -> Result<(), ServiceError> { + let Some(prompt) = self.service.prompt().await else { + return Err(ServiceError::NoSuchObject( + "Prompt does not exist.".to_string(), + )); + }; + + match prompt.role() { + PromptRole::Lock => { + self.prompt( + true, + reply, + exchange, + connection.clone(), + prompt.path().clone(), + prompt.objects().clone(), + ) + .await?; + } + PromptRole::Unlock => { + self.prompt( + false, + reply, + exchange, + connection.clone(), + prompt.path().clone(), + prompt.objects().clone(), + ) + .await?; + } + PromptRole::CreateCollection => todo!(), + }; + + Ok(()) + } + + pub async fn prompt_done( + &self, + #[zbus(object_server)] object_server: &zbus::ObjectServer, + ) -> Result<(), ServiceError> { + if let Some(prompt) = self.service.prompt().await { + object_server.remove::(prompt.path()).await?; + self.service.remove_prompt().await; + } + object_server.remove::(&self.path).await?; + + Ok(()) + } +} + +impl PrompterCallback { + pub async fn new(service: Service) -> Self { + let index = service.prompt_index().await; + Self { + path: OwnedObjectPath::try_from(format!("/org/gnome/keyring/Prompt/p{index}")).unwrap(), + service, + } + } + + pub fn path(&self) -> &OwnedObjectPath { + &self.path + } + + pub async fn object_label(&self, objects: &Vec) -> Option { + for object in objects { + if let Some(collection) = self.service.collection_from_path(object).await { + return Some(collection.label().await); + } + } + None + } + + pub async fn perform_prompt( + connection: zbus::Connection, + path: OwnedObjectPath, + prompt_type: PromptType, + properties: Properties, + exchange: String, + ) -> Result<(), ServiceError> { + let prompter = PrompterProxy::new(&connection).await?; + prompter + .perform_prompt(path, prompt_type, properties, &exchange) + .await?; + + Ok(()) + } + + pub async fn stop_prompting( + connection: zbus::Connection, + path: OwnedObjectPath, + ) -> Result<(), ServiceError> { + let prompter = PrompterProxy::new(&connection).await?; + prompter.stop_prompting(path).await?; + + Ok(()) + } + + pub async fn prompt_completed( + signal_emitter: zbus::object_server::SignalEmitter<'_>, + dismissed: bool, + result: OwnedValue, + ) -> Result<(), ServiceError> { + Prompt::completed(&signal_emitter, dismissed, result).await?; + tracing::debug!("Prompt completed."); + + Ok(()) + } + + pub async fn prompt( + &self, + locked: bool, + reply: Optional, + exchange: &str, + connection: zbus::Connection, + prompt_path: OwnedObjectPath, + objects: Vec, + ) -> Result<(), ServiceError> { + if let Some(reply) = reply.as_ref() { + match reply { + Reply::No => { + // Second PromptReady call and the prompt is dismissed + tracing::debug!("Prompt is being dismissed."); + + tokio::spawn(PrompterCallback::stop_prompting( + connection, + self.path.clone(), + )); + + let signal_emitter = self.service.signal_emitter(prompt_path)?; + let result = Value::new::>(vec![]) + .try_to_owned() + .unwrap(); + + tokio::spawn(PrompterCallback::prompt_completed( + signal_emitter, + true, + result, + )); + } + Reply::Yes => { + // Second PromptReady call with the final exchange + let label = self.object_label(&objects).await.unwrap_or(String::new()); + + // If this is for Unlock, verify the secret + if !locked { + if let Some(secret) = SecretExchange::retrieve_secret( + exchange, + &self.service.secret_exchange_aes_key().await, + ) { + match oo7::file::Keyring::open(&label, secret).await { + Ok(_) => { + tracing::debug!("{} keyring secret matches.", &label); + } + Err(oo7::file::Error::IncorrectSecret) => { + tracing::error!("{} keyring incorrect secret.", &label); + + let properties = Properties::for_unlock( + &label, + "The unlock password was incorrect", + ); + let path = self.path.clone(); + + let server_exchange = + self.service.secret_exchange_pub_key().await; + + tokio::spawn(PrompterCallback::perform_prompt( + connection.clone(), + path, + PromptType::Password, + properties, + server_exchange, + )); + + return Ok(()); + } + Err(err) => { + return Err(ServiceError::ZBus(zbus::Error::FDO(Box::new( + zbus::fdo::Error::Failed(format!( + "Failed to unlock {label} Keyring: {err}." + )), + )))) + } + } + } + } + + let service = self.service.clone(); + let result = Value::new(objects.clone()).try_to_owned().unwrap(); + + tokio::spawn(async move { + let _ = service.set_locked(locked, &objects, true).await; + }); + + tokio::spawn(PrompterCallback::stop_prompting( + connection, + self.path.clone(), + )); + + let signal_emitter = self.service.signal_emitter(prompt_path)?; + + tokio::spawn(PrompterCallback::prompt_completed( + signal_emitter, + false, + result, + )); + } + } + } else { + // First PromptReady call + let secret_exchange = SecretExchange::new().map_err(|err| { + ServiceError::ZBus(zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed( + format!("Failed to generate SecretExchange {err}."), + )))) + })?; + let server_exchange = secret_exchange.begin(); + self.service + .set_secret_exchange_pub_key(server_exchange.clone()) + .await; + + if !locked { + let aes_key = secret_exchange + .create_shared_secret(exchange) + .map_err(|err| { + ServiceError::ZBus(zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed( + format!("Failed to generate AES key for SecretExchange {err}."), + )))) + })?; + self.service.set_secret_exchange_aes_key(aes_key).await; + } + + let label = self.object_label(&objects).await.unwrap_or(String::new()); + let (properties, prompt_type) = if locked { + (Properties::for_lock(&label), PromptType::Confirm) + } else { + (Properties::for_unlock(&label, ""), PromptType::Password) + }; + let path = self.path.clone(); + + tokio::spawn(PrompterCallback::perform_prompt( + connection.clone(), + path, + prompt_type, + properties, + server_exchange, + )); + } + + Ok(()) + } +} diff --git a/server/src/prompt.rs b/server/src/prompt.rs index 3752fb63..f61a60e8 100644 --- a/server/src/prompt.rs +++ b/server/src/prompt.rs @@ -1,25 +1,95 @@ // org.freedesktop.Secret.Prompt use oo7::dbus::ServiceError; -use zbus::{interface, object_server::SignalEmitter, zvariant::OwnedValue}; +use zbus::{ + interface, + object_server::SignalEmitter, + zvariant::{OwnedObjectPath, OwnedValue}, +}; -#[derive(Debug)] -pub struct Prompt {} +use crate::{ + gnome::prompter::{PrompterCallback, PrompterProxy}, + service::Service, +}; + +#[derive(Debug, Clone)] +#[allow(unused)] +pub enum PromptRole { + Lock, + Unlock, + CreateCollection, +} + +#[derive(Debug, Clone)] +pub struct Prompt { + service: Service, + objects: Vec, + role: PromptRole, + path: OwnedObjectPath, +} #[interface(name = "org.freedesktop.Secret.Prompt")] impl Prompt { - pub async fn prompt(&self, _window_id: &str) -> Result<(), ServiceError> { - todo!() + pub async fn prompt( + &self, + _window_id: &str, + #[zbus(connection)] connection: &zbus::Connection, + #[zbus(object_server)] object_server: &zbus::ObjectServer, + ) -> Result<(), ServiceError> { + let callback = PrompterCallback::new(self.service.clone()).await; + let path = callback.path().clone(); + let connection = connection.clone(); + + object_server.at(&path, callback).await?; + tracing::debug!("Prompt `{}` created.", self.path); + + tokio::spawn(async move { + let prompter = PrompterProxy::new(&connection).await.unwrap(); + prompter.begin_prompting(&path).await.unwrap(); + }); + + Ok(()) } - pub async fn dismiss(&self) -> Result<(), ServiceError> { - todo!() + pub async fn dismiss( + &self, + #[zbus(object_server)] object_server: &zbus::ObjectServer, + ) -> Result<(), ServiceError> { + object_server.remove::(&self.path).await?; + self.service.remove_prompt().await; + + Ok(()) } #[zbus(signal, name = "Completed")] - async fn completed( + pub async fn completed( signal_emitter: &SignalEmitter<'_>, dismissed: bool, result: OwnedValue, ) -> zbus::Result<()>; } + +impl Prompt { + pub async fn new(service: Service, objects: Vec, role: PromptRole) -> Self { + let index = service.prompt_index().await; + Self { + path: OwnedObjectPath::try_from(format!("/org/freedesktop/secrets/prompt/p{index}")) + .unwrap(), + service, + objects, + role, + } + } + + pub fn path(&self) -> &OwnedObjectPath { + &self.path + } + + pub fn role(&self) -> &PromptRole { + &self.role + } + + pub fn objects(&self) -> &Vec { + &self.objects + } +} diff --git a/server/src/service.rs b/server/src/service.rs index d34938de..2316b43b 100644 --- a/server/src/service.rs +++ b/server/src/service.rs @@ -18,7 +18,12 @@ use zbus::{ zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}, }; -use crate::{collection::Collection, error::Error, session::Session}; +use crate::{ + collection::Collection, + error::Error, + prompt::{Prompt, PromptRole}, + session::Session, +}; #[derive(Debug, Clone)] pub struct Service { @@ -29,6 +34,12 @@ pub struct Service { // sessions mapped to their corresponding object path on the bus sessions: Arc>>, session_index: Arc>, + prompts: Arc>>, + prompt_index: Arc>, + // SecretExchange public_key + secret_exchange_pub_key: Arc>, + // SecretExchange aes_key + secret_exchange_aes_key: Arc>, } #[zbus::interface(name = "org.freedesktop.Secret.Service")] @@ -130,8 +141,17 @@ impl Service { pub async fn unlock( &self, objects: Vec, + #[zbus(object_server)] object_server: &zbus::ObjectServer, ) -> Result<(Vec, OwnedObjectPath), ServiceError> { - let (unlocked, _not_unlocked) = self.set_locked(false, &objects).await?; + let (unlocked, not_unlocked) = self.set_locked(false, &objects, false).await?; + if !not_unlocked.is_empty() { + let prompt = Prompt::new(self.clone(), not_unlocked, PromptRole::Unlock).await; + self.prompts.lock().await.push(prompt.clone()); + let path = prompt.path().clone(); + + object_server.at(&path, prompt).await?; + return Ok((unlocked, path)); + } Ok((unlocked, OwnedObjectPath::default())) } @@ -140,8 +160,17 @@ impl Service { pub async fn lock( &self, objects: Vec, + #[zbus(object_server)] object_server: &zbus::ObjectServer, ) -> Result<(Vec, OwnedObjectPath), ServiceError> { - let (locked, _not_locked) = self.set_locked(true, &objects).await?; + let (locked, not_locked) = self.set_locked(true, &objects, false).await?; + if !not_locked.is_empty() { + let prompt = Prompt::new(self.clone(), not_locked, PromptRole::Lock).await; + self.prompts.lock().await.push(prompt.clone()); + let path = prompt.path().clone(); + + object_server.at(&path, prompt).await?; + return Ok((locked, path)); + } Ok((locked, OwnedObjectPath::default())) } @@ -273,6 +302,10 @@ impl Service { connection: connection.clone(), sessions: Default::default(), session_index: Default::default(), + prompts: Default::default(), + prompt_index: Default::default(), + secret_exchange_pub_key: Default::default(), + secret_exchange_aes_key: Default::default(), }; object_server @@ -318,6 +351,7 @@ impl Service { &self, locked: bool, objects: &[OwnedObjectPath], + prompt: bool, ) -> Result<(Vec, Vec), ServiceError> { let mut without_prompt = Vec::new(); let mut with_prompt = Vec::new(); @@ -336,7 +370,9 @@ impl Service { without_prompt.push(object.clone()); } else { // TODO: remove this once the prompt implementation is complete. - collection.set_locked(locked).await?; + if prompt { + collection.set_locked(locked).await?; + } with_prompt.push(object.clone()); } break; @@ -355,7 +391,9 @@ impl Service { without_prompt.push(object.clone()); } else { // TODO: remove this once the prompt implementation is complete. - item.set_locked(locked).await?; + if prompt { + item.set_locked(locked).await?; + } with_prompt.push(object.clone()); } break; @@ -410,4 +448,39 @@ impl Service { n_sessions } + + pub async fn prompt_index(&self) -> u32 { + let n_prompts = *self.prompt_index.read().await + 1; + *self.prompt_index.write().await = n_prompts; + + n_prompts + } + + pub async fn prompt(&self) -> Option { + 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(); + } + + pub async fn set_secret_exchange_pub_key(&self, pub_key: String) { + *self.secret_exchange_pub_key.write().await = pub_key; + } + + pub async fn secret_exchange_pub_key(&self) -> String { + self.secret_exchange_pub_key.read().await.clone() + } + + pub async fn set_secret_exchange_aes_key(&self, aes_key: String) { + *self.secret_exchange_aes_key.write().await = aes_key; + } + + pub async fn secret_exchange_aes_key(&self) -> String { + self.secret_exchange_aes_key.read().await.clone() + } }