From db1970623f9e35214960698aacd78fb20ed26ecf Mon Sep 17 00:00:00 2001 From: Dhanuka Warusadura Date: Fri, 10 Jan 2025 13:46:28 +0530 Subject: [PATCH] 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 | 11 +- server/Cargo.toml | 1 + server/src/gnome/mod.rs | 1 + server/src/gnome/secret_exchange.rs | 174 ++++++++++++++++++++++++++++ server/src/main.rs | 1 + 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 server/src/gnome/mod.rs create mode 100644 server/src/gnome/secret_exchange.rs diff --git a/Cargo.lock b/Cargo.lock index e4acf58a..1703f203 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" @@ -553,7 +559,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]] @@ -1211,6 +1217,7 @@ dependencies = [ name = "oo7-daemon" version = "0.3.0" dependencies = [ + "base64", "clap", "enumflags2", "oo7", @@ -1677,7 +1684,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..995b5882 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" oo7 = { workspace = true, features = ["unstable"] } diff --git a/server/src/gnome/mod.rs b/server/src/gnome/mod.rs new file mode 100644 index 00000000..49d3aaee --- /dev/null +++ b/server/src/gnome/mod.rs @@ -0,0 +1 @@ +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..eb1eeecd --- /dev/null +++ b/server/src/gnome/secret_exchange.rs @@ -0,0 +1,174 @@ +// 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::Zeroizing; + +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)] +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 = + Key::generate_aes_key_for_secret_exchange(&self.private_key, &server_public_key)?; + let map = HashMap::from([(PRIVATE, aes_key.as_ref())]); + + Ok(encode(&map)) + } + + pub fn new() -> Result { + let private_key = Key::generate_private_key()?; + let public_key = Key::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) +} + +// Retrieves the secret from final secret exchange string +pub(crate) 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 = vec![0, 1]; + return Some(Zeroizing::new(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(decrypted), + Err(err) => { + tracing::error!("Failed to do crypto decrypt: {}", err); + None + } + } +} + +#[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 = 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 = retrieve_secret(&final_exchange, &peer_1_aes_key).unwrap(); + assert_eq!(b"password".to_vec(), decrypted.to_vec()); + } +} 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;