dbus_secret_service/
session.rs

1// Copyright 2016-2024 dbus-secret-service Contributors
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8// key exchange and crypto for session:
9// 1. Before session negotiation (openSession), set private key and public key using DH method.
10// 2. In session negotiation, send public key.
11// 3. In session negotiation, exchange my public key for server's public key.
12// 4. Use server public key and my private key to derive a shared AES key using HKDF.
13// 5. Format Secret: aes iv is random seed, in secret struct it's the parameter (Array(Byte))
14// 6. Format Secret: encode the secret value for the value field in secret struct.
15//      This encoding uses the aes_key from the associated Session.
16
17use dbus::{
18    arg::{RefArg, Variant},
19    blocking::{Connection, Proxy},
20    Path,
21};
22use zeroize::ZeroizeOnDrop;
23
24use crate::Error;
25
26#[cfg(all(feature = "crypto-rust", feature = "crypto-openssl"))]
27compile_error!("You cannot specify both feature \"crypto-rust\" and feature \"crypto-openssl\"");
28
29/// The algorithms that can be used for encryption-in-transit.
30///
31/// If you are writing an ultra-secure program that accesses the secret service,
32/// and you want to be sure that your secrets are encrypted while being sent to
33/// or retrieved from the service, you can specify either the "crypto-rust" or
34/// the "crypto-openssl" feature to this crate and tell it to use Diffie-Hellman
35/// shared key encryption when passing secrets.  If you don't specify one of those
36/// features, then your only choice is to use no encryption.
37#[derive(Debug, Eq, PartialEq)]
38pub enum EncryptionType {
39    /// Use no encryption when sending/receiving secrets
40    Plain,
41    #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
42    /// Use Diffie-Hellman shared key encryption when sending/receiving secrets
43    Dh,
44}
45
46#[derive(ZeroizeOnDrop)]
47pub(crate) struct EncryptedSecret {
48    #[zeroize(skip)]
49    path: Path<'static>, // the session path
50    salt: Vec<u8>,           // the salt for the encrypted data
51    data: Vec<u8>,           // the encrypted data
52    pub(crate) mime: String, // the mime type of the decrypted data
53}
54
55impl EncryptedSecret {
56    pub(crate) fn from_dbus(value: (Path<'static>, Vec<u8>, Vec<u8>, String)) -> Self {
57        Self {
58            path: value.0,
59            salt: value.1,
60            data: value.2,
61            mime: value.3.to_string(),
62        }
63    }
64
65    pub(crate) fn to_dbus(&self) -> (Path<'static>, Vec<u8>, Vec<u8>, &str) {
66        (
67            self.path.clone(),
68            self.salt.clone(),
69            self.data.clone(),
70            &self.mime,
71        )
72    }
73}
74
75#[derive(ZeroizeOnDrop)]
76pub struct Session {
77    #[zeroize(skip)]
78    pub(crate) path: Path<'static>,
79    #[zeroize(skip)]
80    encryption: EncryptionType,
81    #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
82    shared_key: Option<crypto::AesKey>,
83}
84
85impl std::fmt::Debug for Session {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("Session")
88            .field("path", &self.path)
89            .field(
90                "secrets",
91                if self.is_encrypted() {
92                    &"(Hidden)"
93                } else {
94                    &"None"
95                },
96            )
97            .finish()
98    }
99}
100
101impl Session {
102    pub fn new(p: Proxy<'_, &'_ Connection>, encryption: EncryptionType) -> Result<Session, Error> {
103        use crate::proxy::service::Service;
104        match encryption {
105            EncryptionType::Plain => {
106                use crate::ss::ALGORITHM_PLAIN;
107                // in rust 1.70, this lint applies here even though it shouldn't
108                // because we need an explicit string to interpret as a RefArg
109                #[allow(clippy::box_default)]
110                let bytes_arg = Box::new(String::new()) as Box<dyn RefArg>;
111                let (_, path) = p.open_session(ALGORITHM_PLAIN, Variant(bytes_arg))?;
112                Ok(Session {
113                    path,
114                    encryption,
115                    #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
116                    shared_key: None,
117                })
118            }
119            #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
120            EncryptionType::Dh => {
121                use crate::ss::ALGORITHM_DH;
122                use dbus::arg::cast;
123
124                // crypto: create private and public key
125                let keypair = crypto::Keypair::generate();
126
127                // send our public key with algorithm to service
128                let public_bytes = keypair.public.to_bytes_be();
129                let bytes_arg = Variant(Box::new(public_bytes) as Box<dyn RefArg>);
130                let (out, path) = p.open_session(ALGORITHM_DH, bytes_arg)?;
131
132                // get service public key back and create shared key from it
133                if let Some(server_public_key_bytes) = cast::<Vec<u8>>(&out.0) {
134                    let shared_key = keypair.derive_shared(server_public_key_bytes);
135                    Ok(Session {
136                        path,
137                        encryption,
138                        #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
139                        shared_key: Some(shared_key),
140                    })
141                } else {
142                    Err(Error::Parse)
143                }
144            }
145        }
146    }
147
148    pub fn is_encrypted(&self) -> bool {
149        match self.encryption {
150            EncryptionType::Plain => false,
151            #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
152            EncryptionType::Dh => true,
153        }
154    }
155
156    pub(crate) fn encrypt_secret(&self, data: &[u8], mime: &str) -> EncryptedSecret {
157        match self.encryption {
158            EncryptionType::Plain => EncryptedSecret {
159                path: self.path.clone(),
160                salt: vec![],
161                data: data.to_vec(),
162                mime: mime.to_string(),
163            },
164            #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
165            EncryptionType::Dh => {
166                // encrypt the secret with the data
167                let (encrypted, salt) = crypto::encrypt(data, &self.shared_key.unwrap());
168                EncryptedSecret {
169                    path: self.path.clone(),
170                    salt,
171                    data: encrypted,
172                    mime: mime.to_string(),
173                }
174            }
175        }
176    }
177
178    pub(crate) fn decrypt_secret(&self, secret: EncryptedSecret) -> Result<Vec<u8>, Error> {
179        match self.encryption {
180            EncryptionType::Plain => Ok(secret.data.clone()),
181            #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
182            EncryptionType::Dh => {
183                let clear = crypto::decrypt(&secret.data, &self.shared_key.unwrap(), &secret.salt)?;
184                Ok(clear)
185            }
186        }
187    }
188}
189
190#[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
191mod crypto {
192    use std::ops::{Mul, Rem, Shr};
193
194    use fastrand::Rng;
195    use num::{
196        bigint::BigUint,
197        integer::Integer,
198        traits::{One, Zero},
199        FromPrimitive,
200    };
201    use once_cell::sync::Lazy;
202
203    #[cfg(feature = "crypto-rust")]
204    pub(super) fn encrypt(data: &[u8], key: &AesKey) -> (Vec<u8>, Vec<u8>) {
205        use aes::cipher::block_padding::Pkcs7;
206        use aes::cipher::generic_array::GenericArray;
207        use aes::cipher::{BlockEncryptMut, KeyIvInit};
208
209        type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
210
211        // create the salt for the encryption
212        let aes_iv = salt();
213
214        // convert key and salt to input parameter form
215        let key = GenericArray::from_slice(key);
216        let iv = GenericArray::from_slice(&aes_iv);
217
218        // return encrypted data and salt
219        (
220            Aes128CbcEnc::new(key, iv).encrypt_padded_vec_mut::<Pkcs7>(data),
221            aes_iv.to_vec(),
222        )
223    }
224
225    #[cfg(feature = "crypto-rust")]
226    pub(super) fn decrypt(
227        encrypted_data: &[u8],
228        key: &AesKey,
229        iv: &[u8],
230    ) -> Result<Vec<u8>, crate::Error> {
231        use aes::cipher::block_padding::Pkcs7;
232        use aes::cipher::generic_array::GenericArray;
233        use aes::cipher::{BlockDecryptMut, KeyIvInit};
234
235        type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
236
237        let key = GenericArray::from_slice(key);
238        let iv = GenericArray::from_slice(iv);
239
240        let output = Aes128CbcDec::new(key, iv).decrypt_padded_vec_mut::<Pkcs7>(encrypted_data)?;
241        Ok(output)
242    }
243
244    #[cfg(all(feature = "crypto-openssl", not(feature = "crypto-rust")))]
245    pub(super) fn encrypt(data: &[u8], key: &AesKey) -> (Vec<u8>, Vec<u8>) {
246        use openssl::cipher::Cipher;
247        use openssl::cipher_ctx::CipherCtx;
248
249        // create the salt for the encryption
250        let aes_iv = salt();
251
252        let mut ctx = CipherCtx::new().expect("cipher creation should not fail");
253        ctx.encrypt_init(Some(Cipher::aes_128_cbc()), Some(key), Some(&aes_iv))
254            .expect("cipher init should not fail");
255
256        let mut output = vec![];
257        ctx.cipher_update_vec(data, &mut output)
258            .expect("cipher update should not fail");
259        ctx.cipher_final_vec(&mut output)
260            .expect("cipher final should not fail");
261        (output, aes_iv.to_vec())
262    }
263
264    #[cfg(all(feature = "crypto-openssl", not(feature = "crypto-rust")))]
265    pub(super) fn decrypt(
266        encrypted_data: &[u8],
267        key: &AesKey,
268        iv: &[u8],
269    ) -> Result<Vec<u8>, crate::Error> {
270        use openssl::cipher::Cipher;
271        use openssl::cipher_ctx::CipherCtx;
272
273        let mut ctx = CipherCtx::new().expect("cipher creation should not fail");
274        ctx.decrypt_init(Some(Cipher::aes_128_cbc()), Some(key), Some(iv))
275            .expect("cipher init should not fail");
276
277        let mut output = vec![];
278        ctx.cipher_update_vec(encrypted_data, &mut output)?;
279        ctx.cipher_final_vec(&mut output)?;
280        Ok(output)
281    }
282
283    fn salt() -> [u8; 16] {
284        let mut rng = Rng::new();
285        let mut salt = [0; 16];
286        rng.fill(&mut salt);
287        salt
288    }
289
290    // for key exchange
291    static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_u64(0x2).unwrap());
292    static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
293        BigUint::from_bytes_be(&[
294            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68,
295            0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08,
296            0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A,
297            0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B,
298            0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51,
299            0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9,
300            0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38,
301            0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6,
302            0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
303            0xFF, 0xFF,
304        ])
305    });
306
307    pub(super) type AesKey = [u8; 16];
308
309    #[derive(Clone)]
310    pub(super) struct Keypair {
311        pub(super) private: BigUint,
312        pub(super) public: BigUint,
313    }
314
315    impl Keypair {
316        pub(super) fn generate() -> Self {
317            let mut rng = Rng::new();
318            let mut private_key_bytes = [0; 128];
319            rng.fill(&mut private_key_bytes);
320
321            let private_key = BigUint::from_bytes_be(&private_key_bytes);
322            let public_key = pow_base_exp_mod(&DH_GENERATOR, &private_key, &DH_PRIME);
323
324            Self {
325                private: private_key,
326                public: public_key,
327            }
328        }
329
330        pub(super) fn derive_shared(&self, server_public_key_bytes: &[u8]) -> AesKey {
331            // Derive the shared secret the server and us.
332            let server_public_key = BigUint::from_bytes_be(server_public_key_bytes);
333            let common_secret = pow_base_exp_mod(&server_public_key, &self.private, &DH_PRIME);
334
335            let common_secret_bytes = common_secret.to_bytes_be();
336            let mut common_secret_padded = vec![0; 128 - common_secret_bytes.len()];
337            common_secret_padded.extend(common_secret_bytes);
338
339            // hkdf
340
341            // input keying material
342            let ikm = common_secret_padded;
343            let salt = None;
344
345            // output keying material
346            let mut okm = [0; 16];
347            hkdf(ikm, salt, &mut okm);
348
349            okm
350        }
351    }
352
353    #[cfg(all(feature = "crypto-openssl", not(feature = "crypto-rust")))]
354    pub(super) fn hkdf(ikm: Vec<u8>, salt: Option<&[u8]>, okm: &mut [u8]) {
355        let mut ctx = openssl::pkey_ctx::PkeyCtx::new_id(openssl::pkey::Id::HKDF)
356            .expect("hkdf context should not fail");
357        ctx.derive_init().expect("hkdf derive init should not fail");
358        ctx.set_hkdf_md(openssl::md::Md::sha256())
359            .expect("hkdf set md should not fail");
360
361        ctx.set_hkdf_key(&ikm)
362            .expect("hkdf set key should not fail");
363        if let Some(salt) = salt {
364            ctx.set_hkdf_salt(salt)
365                .expect("hkdf set salt should not fail");
366        }
367
368        ctx.add_hkdf_info(&[]).unwrap();
369        ctx.derive(Some(okm))
370            .expect("hkdf expand should never fail");
371    }
372
373    #[cfg(feature = "crypto-rust")]
374    pub(super) fn hkdf(ikm: Vec<u8>, salt: Option<&[u8]>, okm: &mut [u8]) {
375        use sha2::Sha256;
376
377        let info = [];
378        let (_, hk) = hkdf::Hkdf::<Sha256>::extract(salt, &ikm);
379        hk.expand(&info, okm)
380            .expect("hkdf expand should never fail");
381    }
382
383    /// from https://github.com/plietar/librespot/blob/master/core/src/util/mod.rs#L53
384    pub(super) fn pow_base_exp_mod(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
385        let mut base = base.clone();
386        let mut exp = exp.clone();
387        let mut result: BigUint = One::one();
388
389        while !exp.is_zero() {
390            if exp.is_odd() {
391                result = result.mul(&base).rem(modulus);
392            }
393            exp = exp.shr(1);
394            base = (&base).mul(&base).rem(modulus);
395        }
396
397        result
398    }
399}
400
401#[cfg(test)]
402mod test {
403    use dbus::blocking::Connection;
404
405    use crate::proxy::new_proxy;
406    use crate::ss::SS_DBUS_PATH;
407
408    use super::*;
409
410    #[test]
411    fn should_create_plain_session() {
412        let connection = Connection::new_session().unwrap();
413        let proxy = new_proxy(&connection, SS_DBUS_PATH);
414        let session = Session::new(proxy, EncryptionType::Plain).unwrap();
415        assert!(!session.is_encrypted());
416    }
417
418    #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))]
419    #[test]
420    fn should_create_encrypted_session() {
421        let connection = Connection::new_session().unwrap();
422        let proxy = new_proxy(&connection, SS_DBUS_PATH);
423        let session = Session::new(proxy, EncryptionType::Dh).unwrap();
424        assert!(session.is_encrypted());
425    }
426}