decrypt_cookies/chromium/
builder.rs

1#![expect(clippy::exhaustive_structs, reason = "Allow error")]
2
3use std::{
4    fmt::{Debug, Display},
5    marker::PhantomData,
6    path::{Path, PathBuf},
7};
8
9use chromium_crypto::Decrypter;
10use snafu::{ensure, Location, OptionExt, ResultExt, Snafu};
11use tokio::{fs, join};
12
13use super::{ChromiumCookieGetter, ChromiumGetter, ChromiumLoginGetter};
14use crate::{
15    browser::ChromiumPath,
16    chromium::items::{cookie::cookie_dao::CookiesQuery, passwd::login_data_dao::LoginDataQuery},
17};
18
19// TODO: add browser name in error
20#[derive(Debug)]
21#[derive(Snafu)]
22#[snafu(visibility(pub))]
23#[non_exhaustive]
24pub enum ChromiumBuilderError {
25    #[snafu(display(r#"Not found {}
26The browser is not installed or started with `--user-data-dir` arg
27@:{location}"#, path.display()))]
28    NotFoundBase {
29        path: PathBuf,
30        #[snafu(implicit)]
31        location: Location,
32    },
33    #[snafu(display("{source}\n@:{location}"))]
34    Decrypter {
35        source: chromium_crypto::error::CryptoError,
36        #[snafu(implicit)]
37        location: Location,
38    },
39    #[snafu(display("{source}\n@:{location}"))]
40    Db {
41        source: sea_orm::DbErr,
42        #[snafu(implicit)]
43        location: Location,
44    },
45    #[snafu(display("{source}, path: {}\n@:{location}",path.display()))]
46    Io {
47        source: std::io::Error,
48        path: PathBuf,
49        #[snafu(implicit)]
50        location: Location,
51    },
52    #[cfg(target_os = "windows")]
53    #[snafu(display("{source}\n@:{location}"))]
54    Rawcopy {
55        source: anyhow::Error,
56        #[snafu(implicit)]
57        location: Location,
58    },
59    #[snafu(display("{source}\n@:{location}"))]
60    TokioJoin {
61        source: tokio::task::JoinError,
62        #[snafu(implicit)]
63        location: Location,
64    },
65    #[snafu(display("Can not found home dir\n@:{location}"))]
66    Home {
67        #[snafu(implicit)]
68        location: Location,
69    },
70}
71
72pub type Result<T> = std::result::Result<T, ChromiumBuilderError>;
73
74/// The `to` must have parent dir
75async fn copy<A, A0>(from: A, to: A0) -> Result<()>
76where
77    A: AsRef<Path> + Send,
78    A0: AsRef<Path> + Send,
79{
80    let parent = to
81        .as_ref()
82        .parent()
83        .expect("Get parent dir failed");
84    fs::create_dir_all(parent)
85        .await
86        .with_context(|_| IoSnafu { path: parent.to_owned() })?;
87
88    #[cfg(not(target_os = "windows"))]
89    fs::copy(from.as_ref(), to.as_ref())
90        .await
91        .with_context(|_| IoSnafu { path: from.as_ref().to_owned() })?;
92    #[cfg(target_os = "windows")]
93    {
94        let from_ = from.as_ref().to_owned();
95        let to = to.as_ref().to_owned();
96        tokio::task::spawn_blocking(move || crate::utils::shadow_copy(&from_, &to))
97            .await
98            .context(TokioJoinSnafu)??;
99    };
100
101    Ok(())
102}
103
104#[derive(Clone)]
105#[derive(Debug)]
106#[derive(Default)]
107#[derive(PartialEq, Eq)]
108pub struct ChromiumBuilder<T> {
109    pub(crate) base: Option<PathBuf>,
110    pub(crate) __browser: PhantomData<T>,
111}
112
113impl<B: ChromiumPath> Display for ChromiumBuilder<B> {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.write_fmt(format_args!("{}Builder", B::NAME))
116    }
117}
118
119impl<B: ChromiumPath> ChromiumBuilder<B> {
120    pub const fn new() -> Self {
121        Self {
122            base: None,
123            __browser: PhantomData::<B>,
124        }
125    }
126
127    /// When browser start with `--user-data-dir=DIR` or special other channel
128    pub const fn with_user_data_dir(base: PathBuf) -> Self {
129        Self {
130            base: Some(base),
131            __browser: PhantomData::<B>,
132        }
133    }
134}
135
136impl<B: ChromiumPath + Send + Sync> ChromiumBuilder<B> {
137    fn ensure_base(self) -> Result<PathBuf> {
138        let base = if let Some(base) = self.base {
139            base
140        }
141        else {
142            let mut base = dirs::home_dir().context(HomeSnafu)?;
143
144            base.push(B::BASE);
145            base
146        };
147
148        ensure!(base.exists(), NotFoundBaseSnafu { path: base });
149
150        Ok(base)
151    }
152
153    #[cfg_attr(
154        feature = "tracing",
155        tracing::instrument(name = "Chromium build", skip(self), fields(browser), level = "debug")
156    )]
157    pub async fn build(self) -> Result<ChromiumGetter<B>> {
158        let __browser = self.__browser;
159        let base = self.ensure_base()?;
160
161        #[cfg(feature = "tracing")]
162        {
163            tracing::Span::current().record("browser", B::NAME);
164            tracing::debug!(base = %base.display());
165        };
166
167        let crypto = Self::gen_crypto(&base);
168
169        let (crypto, cookies_query, logins) = join!(
170            crypto,
171            Self::cache_cookies(base.clone()),
172            Self::cache_login(base.clone())
173        );
174
175        let (login_data_query, lfa) = logins?;
176
177        Ok(ChromiumGetter {
178            cookies_query: cookies_query?,
179            login_data_query,
180            login_data_for_account_query: lfa,
181            crypto: crypto?,
182            __browser,
183        })
184    }
185
186    #[cfg_attr(
187        feature = "tracing",
188        tracing::instrument(
189            name = "Chromium Login build",
190            skip(self),
191            fields(browser),
192            level = "debug"
193        )
194    )]
195    pub async fn build_login(self) -> Result<ChromiumLoginGetter<B>> {
196        let __browser = self.__browser;
197        let base = self.ensure_base()?;
198
199        #[cfg(feature = "tracing")]
200        {
201            tracing::Span::current().record("browser", B::NAME);
202            tracing::debug!(base = %base.display());
203        };
204
205        let (crypto, logins) = join!(Self::gen_crypto(&base), Self::cache_login(base.clone()));
206
207        let (login_data_query, lfa) = logins?;
208
209        Ok(ChromiumLoginGetter {
210            login_data_query,
211            login_data_for_account_query: lfa,
212            crypto: crypto?,
213            __browser,
214        })
215    }
216
217    #[cfg_attr(
218        feature = "tracing",
219        tracing::instrument(
220            name = "Chromium Cookie build",
221            skip(self),
222            fields(browser),
223            level = "debug"
224        )
225    )]
226    pub async fn build_cookie(self) -> Result<ChromiumCookieGetter<B>> {
227        let __browser = self.__browser;
228        let base = self.ensure_base()?;
229
230        #[cfg(feature = "tracing")]
231        {
232            tracing::Span::current().record("browser", B::NAME);
233            tracing::debug!(base = %base.display());
234        };
235
236        let crypto = Self::gen_crypto(&base);
237
238        let (crypto, cookies_query) = join!(crypto, Self::cache_cookies(base.clone()));
239
240        Ok(ChromiumCookieGetter {
241            cookies_query: cookies_query?,
242            crypto: crypto?,
243            __browser,
244        })
245    }
246
247    #[cfg_attr(
248        not(target_os = "windows"),
249        expect(unused_variables, reason = "for windows")
250    )]
251    async fn gen_crypto(base: &Path) -> Result<Decrypter> {
252        #[cfg(target_os = "linux")]
253        let crypto = Decrypter::build(B::SAFE_STORAGE, crate::browser::need_safe_storage);
254
255        #[cfg(target_os = "macos")]
256        let crypto = Decrypter::build(B::SAFE_STORAGE, B::SAFE_NAME);
257
258        #[cfg(target_os = "windows")]
259        let crypto = {
260            let key_path = Self::cache_key(base.to_owned()).await?;
261            Decrypter::build(key_path)
262        };
263
264        crypto
265            .await
266            .context(DecrypterSnafu)
267    }
268
269    /// return login and login for account
270    async fn cache_login(base: PathBuf) -> Result<(LoginDataQuery, Option<LoginDataQuery>)> {
271        let login_data = B::login_data(base.clone());
272        let login_data_temp = B::login_data_temp().context(HomeSnafu)?;
273
274        let login_data_for_account = B::login_data_for_account(base.clone());
275        let login_data_for_account_temp = B::login_data_for_account_temp().context(HomeSnafu)?;
276
277        let (lg, lfac) = join!(
278            copy(&login_data, &login_data_temp),
279            copy(&login_data_for_account, &login_data_for_account_temp)
280        );
281        lg?;
282
283        Ok((
284            LoginDataQuery::new(login_data_temp)
285                .await
286                .context(DbSnafu)?,
287            if lfac.is_ok() {
288                LoginDataQuery::new(login_data_for_account_temp)
289                    .await
290                    .ok()
291            }
292            else {
293                None
294            },
295        ))
296    }
297
298    async fn cache_cookies(base: PathBuf) -> Result<CookiesQuery> {
299        let cookies = B::cookies(base.clone());
300        let cookies_temp = B::cookies_temp().context(HomeSnafu)?;
301
302        copy(&cookies, &cookies_temp).await?;
303        CookiesQuery::new(cookies_temp)
304            .await
305            .context(DbSnafu)
306    }
307
308    #[cfg(target_os = "windows")]
309    async fn cache_key(base: PathBuf) -> Result<PathBuf> {
310        let key = B::key(base.clone());
311        let key_temp = B::key_temp().context(HomeSnafu)?;
312
313        copy(&key, &key_temp).await?;
314
315        Ok(key_temp)
316    }
317}