logins/
encryption.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 */
5
6// This is the *local* encryption support - it has nothing to do with the
7// encryption used by sync.
8
9// For context, what "local encryption" means in this context is:
10// * We use regular sqlite, but ensure that sensitive data is encrypted in the DB in the
11//   `secure_fields` column.  The encryption key is managed by the app.
12// * The `decrypt_struct` and `encrypt_struct` functions are used to convert between an encrypted
13//   `secure_fields` string and a decrypted `SecureFields` struct
14// * Most API functions return `EncryptedLogin` which has its data encrypted.
15//
16// This makes life tricky for Sync - sync has its own encryption and its own
17// management of sync keys. The entire records are encrypted on the server -
18// so the record on the server has the plain-text data (which is then
19// encrypted as part of the entire record), so:
20// * When transforming a record from the DB into a Sync record, we need to
21//   *decrypt* the data.
22// * When transforming a record from Sync into a DB record, we need to *encrypt*
23//   the data.
24//
25// So Sync needs to know the key etc, and that needs to get passed down
26// multiple layers, from the app saying "sync now" all the way down to the
27// low level sync code.
28// To make life a little easier, we do that via a struct.
29//
30// Consumers of the Login component have 3 options for setting up encryption:
31//    1. Implement EncryptorDecryptor directly
32//       eg `LoginStore::new(MyEncryptorDecryptor)`
33//    2. Implement KeyManager and use ManagedEncryptorDecryptor
34//       eg `LoginStore::new(ManagedEncryptorDecryptor::new(MyKeyManager))`
35//    3. Generate a single key and create a StaticKeyManager and use it together with
36//       ManagedEncryptorDecryptor
37//       eg `LoginStore::new(ManagedEncryptorDecryptor::new(StaticKeyManager { key: myKey }))`
38//
39//  You can implement EncryptorDecryptor directly to keep full control over the encryption
40//  algorithm. For example, on the desktop, this could make use of NSS's SecretDecoderRing to
41//  achieve transparent key management.
42//
43//  If the application wants to keep the current encryption, like Android and iOS, for example, but
44//  control the key management itself, the KeyManager can be implemented and the encryption can be
45//  done on the Rust side with the ManagedEncryptorDecryptor.
46//
47//  In tests or some command line tools, it can be practical to use a static key that does not
48//  change at runtime and is already present when the LoginsStore is initialized. In this case, it
49//  makes sense to use the provided StaticKeyManager.
50
51// work around not yet having https://github.com/mozilla/uniffi-rs/pull/2963.
52#![allow(const_evaluatable_unchecked)]
53
54use crate::error::*;
55use std::sync::Arc;
56
57#[cfg(feature = "keydb")]
58use futures::executor::block_on;
59
60#[cfg(feature = "keydb")]
61use async_trait::async_trait;
62
63#[cfg(feature = "keydb")]
64use parking_lot::RwLock;
65
66#[cfg(feature = "keydb")]
67use nss_as::assert_initialized as assert_nss_initialized;
68#[cfg(feature = "keydb")]
69use nss_as::pk11::sym_key::{
70    authenticate_with_primary_password, authentication_with_primary_password_is_needed,
71    get_or_create_aes256_key,
72};
73
74/// This is the generic EncryptorDecryptor trait, as handed over to the Store during initialization.
75/// Consumers can implement either this generic trait and bring in their own crypto, or leverage the
76/// ManagedEncryptorDecryptor below, which provides encryption algorithms out of the box.
77///
78/// Note that EncryptorDecryptor must not call any LoginStore methods. The login store can call out
79/// to the EncryptorDecryptor when it's internal mutex is held so calling back in to the LoginStore
80/// may deadlock.
81#[uniffi::trait_interface]
82pub trait EncryptorDecryptor: Send + Sync {
83    fn encrypt(&self, cleartext: Vec<u8>) -> ApiResult<Vec<u8>>;
84    fn decrypt(&self, ciphertext: Vec<u8>) -> ApiResult<Vec<u8>>;
85}
86
87impl<T: EncryptorDecryptor> EncryptorDecryptor for Arc<T> {
88    fn encrypt(&self, clearbytes: Vec<u8>) -> ApiResult<Vec<u8>> {
89        (**self).encrypt(clearbytes)
90    }
91
92    fn decrypt(&self, cipherbytes: Vec<u8>) -> ApiResult<Vec<u8>> {
93        (**self).decrypt(cipherbytes)
94    }
95}
96
97/// The ManagedEncryptorDecryptor makes use of the NSS provided cryptographic algorithms. The
98/// ManagedEncryptorDecryptor uses a KeyManager for encryption key retrieval.
99pub struct ManagedEncryptorDecryptor {
100    key_manager: Arc<dyn KeyManager>,
101}
102
103impl ManagedEncryptorDecryptor {
104    #[uniffi::constructor()]
105    pub fn new(key_manager: Arc<dyn KeyManager>) -> Self {
106        Self { key_manager }
107    }
108}
109
110impl EncryptorDecryptor for ManagedEncryptorDecryptor {
111    fn encrypt(&self, clearbytes: Vec<u8>) -> ApiResult<Vec<u8>> {
112        let keybytes = self
113            .key_manager
114            .get_key()
115            .map_err(|_| LoginsApiError::MissingKey)?;
116        let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?;
117
118        let encdec = jwcrypto::EncryptorDecryptor::new(key)
119            .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?;
120
121        let cleartext =
122            std::str::from_utf8(&clearbytes).map_err(|e| LoginsApiError::EncryptionFailed {
123                reason: e.to_string(),
124            })?;
125        encdec
126            .encrypt(cleartext)
127            .map_err(
128                |e: jwcrypto::JwCryptoError| LoginsApiError::EncryptionFailed {
129                    reason: e.to_string(),
130                },
131            )
132            .map(|text| text.into())
133    }
134
135    fn decrypt(&self, cipherbytes: Vec<u8>) -> ApiResult<Vec<u8>> {
136        let keybytes = self
137            .key_manager
138            .get_key()
139            .map_err(|_| LoginsApiError::MissingKey)?;
140        let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?;
141
142        let encdec = jwcrypto::EncryptorDecryptor::new(key)
143            .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?;
144
145        let ciphertext =
146            std::str::from_utf8(&cipherbytes).map_err(|e| LoginsApiError::DecryptionFailed {
147                reason: e.to_string(),
148            })?;
149        encdec
150            .decrypt(ciphertext)
151            .map_err(
152                |e: jwcrypto::JwCryptoError| LoginsApiError::DecryptionFailed {
153                    reason: e.to_string(),
154                },
155            )
156            .map(|text| text.into())
157    }
158}
159
160/// Consumers can implement the KeyManager in combination with the ManagedEncryptorDecryptor to hand
161/// over the encryption key whenever encryption or decryption happens.
162#[uniffi::trait_interface]
163pub trait KeyManager: Send + Sync {
164    fn get_key(&self) -> ApiResult<Vec<u8>>;
165}
166
167/// Last but not least we provide a StaticKeyManager, which can be
168/// used in cases where there is a single key during runtime, for example in tests.
169pub struct StaticKeyManager {
170    key: String,
171}
172
173impl StaticKeyManager {
174    pub fn new(key: String) -> Self {
175        Self { key }
176    }
177}
178
179impl KeyManager for StaticKeyManager {
180    #[handle_error(Error)]
181    fn get_key(&self) -> ApiResult<Vec<u8>> {
182        Ok(self.key.as_bytes().into())
183    }
184}
185
186/// `PrimaryPasswordAuthenticator` is used in conjunction with `NSSKeyManager` to provide the
187/// primary password and the success or failure actions of the authentication process.
188#[cfg(feature = "keydb")]
189#[uniffi::export(with_foreign)]
190#[async_trait]
191pub trait PrimaryPasswordAuthenticator: Send + Sync {
192    /// Get a primary password for authentication, otherwise return the
193    /// AuthenticationCancelled error to cancel the authentication process.
194    async fn get_primary_password(&self) -> ApiResult<String>;
195    async fn on_authentication_success(&self) -> ApiResult<()>;
196    async fn on_authentication_failure(&self) -> ApiResult<()>;
197}
198
199/// Use the `NSSKeyManager` to use NSS for key management.
200///
201/// NSS stores keys in `key4.db` within the profile and wraps the key with a key derived from the
202/// primary password, if set. It defers to the provided `PrimaryPasswordAuthenticator`
203/// implementation to handle user authentication.  Note that if no primary password is set, the
204/// wrapping key is deterministically derived from an empty string.
205///
206/// Make sure to initialize NSS using `ensure_initialized_with_profile_dir` before creating a
207/// NSSKeyManager.
208///
209/// The key is cached after the first retrieval, since fetching it from NSS costs at least one
210/// token round-trip. The cache is dropped whenever the token turns out to be locked again.
211///
212/// # Examples
213/// ```no_run
214/// use async_trait::async_trait;
215/// use logins::encryption::KeyManager;
216/// use logins::{PrimaryPasswordAuthenticator, LoginsApiError, NSSKeyManager};
217/// use std::sync::Arc;
218///
219/// struct MyPrimaryPasswordAuthenticator {}
220///
221/// #[async_trait]
222/// impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator {
223///     async fn get_primary_password(&self) -> Result<String, LoginsApiError> {
224///         // Most likely, you would want to prompt for a password.
225///         // let password = prompt_string("primary password").unwrap_or_default();
226///         Ok("secret".to_string())
227///     }
228///
229///     async fn on_authentication_success(&self) -> Result<(), LoginsApiError> {
230///         println!("success");
231///         Ok(())
232///     }
233///
234///     async fn on_authentication_failure(&self) -> Result<(), LoginsApiError> {
235///         println!("this did not work, please try again:");
236///         Ok(())
237///     }
238/// }
239/// let key_manager = NSSKeyManager::new(Arc::new(MyPrimaryPasswordAuthenticator {}));
240/// assert_eq!(key_manager.get_key().unwrap().len(), 63);
241/// ```
242#[cfg(feature = "keydb")]
243#[derive(uniffi::Object)]
244pub struct NSSKeyManager {
245    primary_password_authenticator: Arc<dyn PrimaryPasswordAuthenticator>,
246    cached_key: RwLock<Option<Vec<u8>>>,
247}
248
249#[cfg(feature = "keydb")]
250#[uniffi::export]
251impl NSSKeyManager {
252    /// Initialize new `NSSKeyManager` with a given `PrimaryPasswordAuthenticator`.
253    /// There must be a previous initializiation of NSS before initializing
254    /// `NSSKeyManager`, otherwise this panics.
255    #[uniffi::constructor()]
256    pub fn new(primary_password_authenticator: Arc<dyn PrimaryPasswordAuthenticator>) -> Self {
257        assert_nss_initialized();
258        Self {
259            primary_password_authenticator,
260            cached_key: RwLock::new(None),
261        }
262    }
263
264    pub fn into_dyn_key_manager(self: Arc<Self>) -> Arc<dyn KeyManager> {
265        self
266    }
267}
268
269/// Identifier for the logins key, under which the key is stored in NSS.
270#[cfg(feature = "keydb")]
271static KEY_NAME: &str = "as-logins-key";
272
273// wrapp `authentication_with_primary_password_is_needed` into an ApiResult
274#[cfg(feature = "keydb")]
275fn api_authentication_with_primary_password_is_needed() -> ApiResult<bool> {
276    authentication_with_primary_password_is_needed().map_err(|e: nss_as::Error| {
277        LoginsApiError::NSSAuthenticationError {
278            reason: e.to_string(),
279        }
280    })
281}
282
283// wrapp `authenticate_with_primary_password` into an ApiResult
284#[cfg(feature = "keydb")]
285fn api_authenticate_with_primary_password(primary_password: &str) -> ApiResult<bool> {
286    authenticate_with_primary_password(primary_password).map_err(|e: nss_as::Error| {
287        LoginsApiError::NSSAuthenticationError {
288            reason: e.to_string(),
289        }
290    })
291}
292
293#[cfg(feature = "keydb")]
294impl KeyManager for NSSKeyManager {
295    fn get_key(&self) -> ApiResult<Vec<u8>> {
296        if api_authentication_with_primary_password_is_needed()? {
297            // The token locked again since we cached the key, so the cached copy must go.
298            *self.cached_key.write() = None;
299
300            let primary_password =
301                block_on(self.primary_password_authenticator.get_primary_password())?;
302            let mut result = api_authenticate_with_primary_password(&primary_password)?;
303
304            if result {
305                block_on(
306                    self.primary_password_authenticator
307                        .on_authentication_success(),
308                )?;
309            } else {
310                while !result {
311                    block_on(
312                        self.primary_password_authenticator
313                            .on_authentication_failure(),
314                    )?;
315
316                    let primary_password =
317                        block_on(self.primary_password_authenticator.get_primary_password())?;
318                    result = api_authenticate_with_primary_password(&primary_password)?;
319                }
320                block_on(
321                    self.primary_password_authenticator
322                        .on_authentication_success(),
323                )?;
324            }
325        }
326
327        let cached = self.cached_key.read().clone();
328        if let Some(bytes) = cached {
329            return Ok(bytes);
330        }
331
332        let key = get_or_create_aes256_key(KEY_NAME).map_err(|_| LoginsApiError::MissingKey)?;
333        let mut bytes: Vec<u8> = Vec::new();
334        serde_json::to_writer(
335            &mut bytes,
336            &jwcrypto::Jwk::new_direct_from_bytes(None, &key),
337        )
338        .unwrap();
339        *self.cached_key.write() = Some(bytes.clone());
340        Ok(bytes)
341    }
342}
343
344#[handle_error(Error)]
345pub fn create_canary(text: &str, key: &str) -> ApiResult<String> {
346    Ok(jwcrypto::EncryptorDecryptor::new(key)?.create_canary(text)?)
347}
348
349pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult<bool> {
350    let encdec = jwcrypto::EncryptorDecryptor::new(key)
351        .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?;
352    Ok(encdec.check_canary(canary, text).unwrap_or(false))
353}
354
355#[handle_error(Error)]
356pub fn create_key() -> ApiResult<String> {
357    Ok(jwcrypto::EncryptorDecryptor::create_key()?)
358}
359
360#[cfg(test)]
361pub mod test_utils {
362    use super::*;
363    use serde::{de::DeserializeOwned, Serialize};
364
365    lazy_static::lazy_static! {
366        pub static ref TEST_ENCRYPTION_KEY: String = serde_json::to_string(&jwcrypto::Jwk::new_direct_key(Some("test-key".to_string())).unwrap()).unwrap();
367        pub static ref TEST_ENCDEC: Arc<ManagedEncryptorDecryptor> = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(StaticKeyManager { key: TEST_ENCRYPTION_KEY.clone() })));
368    }
369
370    pub fn encrypt_struct<T: Serialize>(fields: &T) -> String {
371        let string = serde_json::to_string(fields).unwrap();
372        let cipherbytes = TEST_ENCDEC.encrypt(string.as_bytes().into()).unwrap();
373        std::str::from_utf8(&cipherbytes).unwrap().to_owned()
374    }
375    pub fn decrypt_struct<T: DeserializeOwned>(ciphertext: String) -> T {
376        let jsonbytes = TEST_ENCDEC.decrypt(ciphertext.as_bytes().into()).unwrap();
377        serde_json::from_str(std::str::from_utf8(&jsonbytes).unwrap()).unwrap()
378    }
379}
380
381#[cfg(not(feature = "keydb"))]
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use nss_as::ensure_initialized;
386
387    #[test]
388    fn test_static_key_manager() {
389        ensure_initialized();
390        let key = create_key().unwrap();
391        let key_manager = StaticKeyManager { key: key.clone() };
392        assert_eq!(key.as_bytes(), key_manager.get_key().unwrap());
393    }
394
395    #[test]
396    fn test_managed_encdec_with_invalid_key() {
397        ensure_initialized();
398        let key_manager = Arc::new(StaticKeyManager {
399            key: "bad_key".to_owned(),
400        });
401        let encdec = ManagedEncryptorDecryptor { key_manager };
402        assert!(matches!(
403            encdec.encrypt("secret".as_bytes().into()).err().unwrap(),
404            LoginsApiError::InvalidKey
405        ));
406    }
407
408    #[test]
409    fn test_managed_encdec_with_missing_key() {
410        ensure_initialized();
411        struct MyKeyManager {}
412        impl KeyManager for MyKeyManager {
413            fn get_key(&self) -> ApiResult<Vec<u8>> {
414                Err(LoginsApiError::MissingKey)
415            }
416        }
417        let key_manager = Arc::new(MyKeyManager {});
418        let encdec = ManagedEncryptorDecryptor { key_manager };
419        assert!(matches!(
420            encdec.encrypt("secret".as_bytes().into()).err().unwrap(),
421            LoginsApiError::MissingKey
422        ));
423    }
424
425    #[test]
426    fn test_managed_encdec() {
427        ensure_initialized();
428        let key = create_key().unwrap();
429        let key_manager = Arc::new(StaticKeyManager { key });
430        let encdec = ManagedEncryptorDecryptor { key_manager };
431        let cleartext = "secret";
432        let ciphertext = encdec.encrypt(cleartext.as_bytes().into()).unwrap();
433        assert_eq!(
434            encdec.decrypt(ciphertext.clone()).unwrap(),
435            cleartext.as_bytes()
436        );
437        let other_encdec = ManagedEncryptorDecryptor {
438            key_manager: Arc::new(StaticKeyManager {
439                key: create_key().unwrap(),
440            }),
441        };
442
443        assert_eq!(
444            other_encdec.decrypt(ciphertext).err().unwrap().to_string(),
445            "decryption failed: Crypto error: NSS error: NSS error: -8190 "
446        );
447    }
448
449    #[test]
450    fn test_key_error() {
451        let storage_err = jwcrypto::EncryptorDecryptor::new("bad-key").err().unwrap();
452        println!("{storage_err:?}");
453        assert!(matches!(storage_err, jwcrypto::JwCryptoError::InvalidKey));
454    }
455
456    #[test]
457    fn test_canary_functionality() {
458        ensure_initialized();
459        const CANARY_TEXT: &str = "Arbitrary sequence of text";
460        let key = create_key().unwrap();
461        let canary = create_canary(CANARY_TEXT, &key).unwrap();
462        assert!(check_canary(&canary, CANARY_TEXT, &key).unwrap());
463
464        let different_key = create_key().unwrap();
465        assert!(!check_canary(&canary, CANARY_TEXT, &different_key).unwrap());
466
467        let bad_key = "bad_key".to_owned();
468        assert!(matches!(
469            check_canary(&canary, CANARY_TEXT, &bad_key).err().unwrap(),
470            LoginsApiError::InvalidKey
471        ));
472    }
473}
474
475#[cfg(feature = "keydb")]
476#[cfg(test)]
477mod tests_keydb {
478    use super::*;
479    use nss_as::ensure_initialized_with_profile_dir;
480    use std::path::PathBuf;
481
482    struct MockPrimaryPasswordAuthenticator {
483        password: String,
484    }
485
486    #[async_trait]
487    impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator {
488        async fn get_primary_password(&self) -> ApiResult<String> {
489            Ok(self.password.clone())
490        }
491        async fn on_authentication_success(&self) -> ApiResult<()> {
492            Ok(())
493        }
494        async fn on_authentication_failure(&self) -> ApiResult<()> {
495            Ok(())
496        }
497    }
498
499    fn profile_path() -> PathBuf {
500        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
501            .join("../support/rc_crypto/nss/fixtures/profile")
502    }
503
504    #[test]
505    fn test_ensure_initialized_with_profile_dir() {
506        ensure_initialized_with_profile_dir(profile_path());
507    }
508
509    #[test]
510    fn test_create_key() {
511        ensure_initialized_with_profile_dir(profile_path());
512        let key = create_key().unwrap();
513        assert_eq!(key.len(), 63)
514    }
515
516    #[test]
517    fn test_nss_key_manager() {
518        ensure_initialized_with_profile_dir(profile_path());
519        // `password` is the primary password of the profile fixture
520        let mock_primary_password_authenticator = MockPrimaryPasswordAuthenticator {
521            password: "password".to_string(),
522        };
523        let nss_key_manager = NSSKeyManager::new(Arc::new(mock_primary_password_authenticator));
524        // key from fixtures/profile/key4.db
525        let expected = [
526            123, 34, 107, 116, 121, 34, 58, 34, 111, 99, 116, 34, 44, 34, 107, 34, 58, 34, 66, 74,
527            104, 84, 108, 103, 51, 118, 56, 49, 65, 66, 51, 118, 87, 50, 71, 122, 54, 104, 69, 54,
528            84, 116, 75, 83, 112, 85, 102, 84, 86, 75, 73, 83, 99, 74, 45, 77, 78, 83, 67, 117, 99,
529            34, 125,
530        ]
531        .to_vec();
532        assert_eq!(nss_key_manager.get_key().unwrap(), expected);
533    }
534
535    #[test]
536    fn test_nss_key_manager_caching() {
537        ensure_initialized_with_profile_dir(profile_path());
538        // `password` is the primary password of the profile fixture
539        let nss_key_manager = NSSKeyManager::new(Arc::new(MockPrimaryPasswordAuthenticator {
540            password: "password".to_string(),
541        }));
542
543        let key = nss_key_manager.get_key().unwrap();
544        assert_eq!(*nss_key_manager.cached_key.read(), Some(key.clone()));
545
546        // A sentinel in the cache tells a cached key apart from a freshly fetched one.
547        let sentinel = b"sentinel".to_vec();
548        *nss_key_manager.cached_key.write() = Some(sentinel.clone());
549        assert_eq!(nss_key_manager.get_key().unwrap(), sentinel);
550
551        // Authenticating with a wrong password logs out of the token, so it is locked again and
552        // the cache must be dropped.
553        assert!(!authenticate_with_primary_password("wrong password").unwrap());
554        assert_eq!(nss_key_manager.get_key().unwrap(), key);
555        assert_eq!(*nss_key_manager.cached_key.read(), Some(key));
556    }
557
558    #[test]
559    fn test_primary_password_authentication() {
560        ensure_initialized_with_profile_dir(profile_path());
561        assert!(authenticate_with_primary_password("password").unwrap());
562    }
563}