Skip to main content

browser_commander/browser/migration/
os_crypt_keys.rs

1//! Resolve the OSCrypt keys a migration needs: the source profile's key (to
2//! decrypt) and the target profile's key (to re-encrypt). Mirrors
3//! `js/src/browser/migration/os-crypt-keys.js`.
4//!
5//! Both sides reuse the cookie key handling so passwords and cookies share one
6//! implementation of the per-platform keystore. Every keystore call is
7//! injectable through [`KeystoreHooks`] so the whole thing runs on Linux in
8//! unit tests.
9//!
10//! On macOS and Linux the Safe Storage key is per application, not per
11//! profile, so the target key is looked up for the launching browser channel.
12//! On Windows the target key does not exist yet: the caller generates one with
13//! `create_windows_profile_key` and writes it into the target `Local State`.
14
15use std::collections::HashMap;
16use std::fmt;
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, Mutex};
19
20use anyhow::{anyhow, Result};
21
22use crate::browser::browser_cookie_credentials::{
23    read_safe_storage_password, read_windows_encryption_key,
24};
25use crate::browser::browser_cookie_crypto::derive_chromium_cookie_key;
26
27/// Reads a Safe Storage password for `(browser, platform)`.
28pub type SafeStoragePasswordReader = Arc<dyn Fn(&str, &str) -> Result<String> + Send + Sync>;
29/// Reads and DPAPI-unwraps the key in a Windows `Local State` file.
30pub type WindowsKeyReader = Arc<dyn Fn(&Path) -> Result<Vec<u8>> + Send + Sync>;
31
32/// The OS keystore calls a migration makes. The defaults use the same
33/// command-stream backed readers as `read_browser_cookies`.
34#[derive(Clone)]
35pub struct KeystoreHooks {
36    /// macOS Keychain / libsecret / KWallet Safe Storage password reader.
37    pub read_safe_storage_password: SafeStoragePasswordReader,
38    /// Windows `Local State` `os_crypt.encrypted_key` reader.
39    pub read_windows_encryption_key: WindowsKeyReader,
40}
41
42impl Default for KeystoreHooks {
43    fn default() -> Self {
44        Self {
45            read_safe_storage_password: Arc::new(read_safe_storage_password),
46            read_windows_encryption_key: Arc::new(read_windows_encryption_key),
47        }
48    }
49}
50
51impl fmt::Debug for KeystoreHooks {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        formatter
54            .debug_struct("KeystoreHooks")
55            .finish_non_exhaustive()
56    }
57}
58
59/// Resolves the source profile's decryption key for an encryption prefix
60/// (`v10`/`v11`). Linux uses a hardcoded key for `v10` and the keyring for
61/// `v11`. Keys are cached per prefix, including failures.
62pub type SourceKeyResolver = Arc<dyn Fn(&str) -> Result<Vec<u8>> + Send + Sync>;
63
64/// Build a decryptor key resolver for a source profile.
65pub(crate) fn create_source_key_resolver(
66    browser: &str,
67    platform: &str,
68    local_state_path: Option<PathBuf>,
69    hooks: KeystoreHooks,
70) -> SourceKeyResolver {
71    let browser = browser.to_string();
72    let platform = platform.to_string();
73    let cache: Mutex<HashMap<String, Result<Vec<u8>, String>>> = Mutex::new(HashMap::new());
74    Arc::new(move |prefix: &str| {
75        let mut cache = cache
76            .lock()
77            .unwrap_or_else(|poisoned| poisoned.into_inner());
78        let entry = cache.entry(prefix.to_string()).or_insert_with(|| {
79            resolve_source_key(
80                &browser,
81                &platform,
82                prefix,
83                local_state_path.as_deref(),
84                &hooks,
85            )
86            .map_err(|error| format!("{error:#}"))
87        });
88        entry.clone().map_err(|message| anyhow!(message))
89    })
90}
91
92fn resolve_source_key(
93    browser: &str,
94    platform: &str,
95    prefix: &str,
96    local_state_path: Option<&Path>,
97    hooks: &KeystoreHooks,
98) -> Result<Vec<u8>> {
99    match platform {
100        "win32" => {
101            let path = local_state_path
102                .ok_or_else(|| anyhow!("The source Local State path is unknown"))?;
103            (hooks.read_windows_encryption_key)(path)
104        }
105        // Chromium's legacy Linux v10 format uses this fixed fallback secret.
106        // Read it only to decrypt an existing source profile; target values
107        // use the launching profile's key from Safe Storage below.
108        "linux" if prefix == "v10" => derive_chromium_cookie_key("peanuts", "linux"),
109        "linux" | "darwin" => {
110            let password = (hooks.read_safe_storage_password)(browser, platform)?;
111            derive_chromium_cookie_key(&password, platform)
112        }
113        _ => Err(anyhow!("OSCrypt keys are unsupported on {platform}")),
114    }
115}
116
117/// A target profile's encryption key and the version prefix to write.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct TargetKey {
120    /// Raw AES key.
121    pub key: Vec<u8>,
122    /// Version prefix written before each encrypted value (`v10` or `v11`).
123    pub prefix: String,
124}
125
126/// Resolve the target (launching) profile's encryption key on macOS/Linux.
127pub(crate) fn resolve_target_key(
128    browser: &str,
129    platform: &str,
130    hooks: &KeystoreHooks,
131) -> Result<TargetKey> {
132    if platform == "darwin" || platform == "linux" {
133        let password = (hooks.read_safe_storage_password)(browser, platform)?;
134        return Ok(TargetKey {
135            key: derive_chromium_cookie_key(&password, platform)?,
136            prefix: "v11".to_string(),
137        });
138    }
139    Err(anyhow!(
140        "resolve_target_key does not derive a Windows key; use create_windows_profile_key"
141    ))
142}
143
144pub(crate) use crate::browser::browser_profile_files::local_state_path_for_profile;