1use crate::prelude::*;
2
3use std::io::{Read as _, Write as _};
4use std::sync::{Arc, OnceLock};
5
6use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
7
8static CACHED: OnceLock<Arc<Config>> = OnceLock::new();
9
10#[derive(serde::Serialize, serde::Deserialize, Debug)]
11pub struct Config {
12 pub email: Option<String>,
13 pub sso_id: Option<String>,
14 pub base_url: Option<String>,
15 pub identity_url: Option<String>,
16 pub ui_url: Option<String>,
17 pub notifications_url: Option<String>,
18 #[serde(default = "default_lock_timeout")]
19 pub lock_timeout: u64,
20 #[serde(default = "default_sync_interval")]
21 pub sync_interval: u64,
22 #[serde(default = "default_pinentry")]
23 pub pinentry: String,
24 pub client_cert_path: Option<std::path::PathBuf>,
25 #[serde(default)]
26 pub ssh_confirm_sign: bool,
27 #[serde(default = "default_macos_unlock_dialog")]
32 pub macos_unlock_dialog: bool,
33 #[serde(default = "default_logging")]
34 pub logging: bool,
35 #[serde(
36 default,
37 with = "touchid_gate_serde",
38 skip_serializing_if = "is_touchid_gate_off"
39 )]
40 pub touchid_gate: crate::touchid::Gate,
41 #[serde(skip_serializing)]
43 pub device_id: Option<String>,
44}
45
46impl Default for Config {
47 fn default() -> Self {
48 Self {
49 email: None,
50 sso_id: None,
51 base_url: None,
52 identity_url: None,
53 ui_url: None,
54 notifications_url: None,
55 lock_timeout: default_lock_timeout(),
56 sync_interval: default_sync_interval(),
57 pinentry: default_pinentry(),
58 client_cert_path: None,
59 ssh_confirm_sign: false,
60 macos_unlock_dialog: default_macos_unlock_dialog(),
61 logging: default_logging(),
62 touchid_gate: crate::touchid::Gate::Off,
63 device_id: None,
64 }
65 }
66}
67
68pub fn default_lock_timeout() -> u64 {
69 3600
70}
71
72pub fn default_sync_interval() -> u64 {
73 3600
74}
75
76pub fn default_pinentry() -> String {
77 "pinentry".to_string()
78}
79
80pub const fn default_macos_unlock_dialog() -> bool {
81 cfg!(target_os = "macos")
82}
83
84pub const fn default_logging() -> bool {
85 false
86}
87
88#[allow(clippy::trivially_copy_pass_by_ref)]
89fn is_touchid_gate_off(g: &crate::touchid::Gate) -> bool {
90 matches!(g, crate::touchid::Gate::Off)
91}
92
93mod touchid_gate_serde {
94 use std::str::FromStr as _;
95
96 use serde::{Deserialize as _, Deserializer, Serializer};
97
98 #[allow(clippy::trivially_copy_pass_by_ref)]
99 pub fn serialize<S: Serializer>(
100 g: &crate::touchid::Gate,
101 s: S,
102 ) -> Result<S::Ok, S::Error> {
103 s.serialize_str(&g.to_string())
104 }
105
106 pub fn deserialize<'de, D: Deserializer<'de>>(
107 d: D,
108 ) -> Result<crate::touchid::Gate, D::Error> {
109 let s = String::deserialize(d)?;
110 crate::touchid::Gate::from_str(&s).map_err(serde::de::Error::custom)
111 }
112}
113
114impl Config {
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn load() -> Result<Self> {
120 let file = crate::dirs::config_file();
121 let mut fh = std::fs::File::open(&file).map_err(|source| {
122 Error::LoadConfig {
123 source,
124 file: file.clone(),
125 }
126 })?;
127 let mut json = String::new();
128 fh.read_to_string(&mut json)
129 .map_err(|source| Error::LoadConfig {
130 source,
131 file: file.clone(),
132 })?;
133 let mut slf: Self = serde_json::from_str(&json)
134 .map_err(|source| Error::LoadConfigJson { source, file })?;
135 if slf.lock_timeout == 0 {
136 log::warn!("lock_timeout must be greater than 0");
137 slf.lock_timeout = default_lock_timeout();
138 }
139 Ok(slf)
140 }
141
142 pub async fn load_async() -> Result<Self> {
143 let file = crate::dirs::config_file();
144 let mut fh =
145 tokio::fs::File::open(&file).await.map_err(|source| {
146 Error::LoadConfigAsync {
147 source,
148 file: file.clone(),
149 }
150 })?;
151 let mut json = String::new();
152 fh.read_to_string(&mut json).await.map_err(|source| {
153 Error::LoadConfigAsync {
154 source,
155 file: file.clone(),
156 }
157 })?;
158 let mut slf: Self = serde_json::from_str(&json)
159 .map_err(|source| Error::LoadConfigJson { source, file })?;
160 if slf.lock_timeout == 0 {
161 log::warn!("lock_timeout must be greater than 0");
162 slf.lock_timeout = default_lock_timeout();
163 }
164 Ok(slf)
165 }
166
167 pub fn save(&self) -> Result<()> {
168 use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
169 let file = crate::dirs::config_file();
170 std::fs::create_dir_all(file.parent().unwrap()).map_err(
173 |source| Error::SaveConfig {
174 source,
175 file: file.clone(),
176 },
177 )?;
178 let mut fh = std::fs::OpenOptions::new()
179 .write(true)
180 .create(true)
181 .truncate(true)
182 .mode(0o600)
183 .open(&file)
184 .map_err(|source| Error::SaveConfig {
185 source,
186 file: file.clone(),
187 })?;
188 fh.set_permissions(std::fs::Permissions::from_mode(0o600))
192 .map_err(|source| Error::SaveConfig {
193 source,
194 file: file.clone(),
195 })?;
196 fh.write_all(
197 serde_json::to_string(self)
198 .map_err(|source| Error::SaveConfigJson {
199 source,
200 file: file.clone(),
201 })?
202 .as_bytes(),
203 )
204 .map_err(|source| Error::SaveConfig { source, file })?;
205 Ok(())
206 }
207
208 pub fn load_cached() -> Result<Arc<Self>> {
213 if let Some(c) = CACHED.get() {
214 return Ok(Arc::clone(c));
215 }
216 let loaded = Arc::new(Self::load()?);
217 Ok(Arc::clone(CACHED.get_or_init(|| loaded)))
218 }
219
220 pub fn validate() -> Result<()> {
221 let config = Self::load_cached()?;
222 if config.email.is_none() {
223 return Err(Error::ConfigMissingEmail);
224 }
225 Ok(())
226 }
227
228 pub fn base_url(&self) -> String {
229 self.base_url.clone().map_or_else(
230 || "https://api.bitwarden.com".to_string(),
231 |url| {
232 let clean_url = url.trim_end_matches('/');
233 if clean_url == "https://api.bitwarden.eu" {
234 "https://api.bitwarden.eu".to_string()
235 } else {
236 format!("{clean_url}/api")
237 }
238 },
239 )
240 }
241
242 pub fn identity_url(&self) -> String {
243 self.identity_url.clone().unwrap_or_else(|| {
244 self.base_url.clone().map_or_else(
245 || "https://identity.bitwarden.com".to_string(),
246 |url| {
247 let clean_url = url.trim_end_matches('/');
248 if clean_url == "https://api.bitwarden.eu" {
249 "https://identity.bitwarden.eu".to_string()
250 } else {
251 format!("{clean_url}/identity")
252 }
253 },
254 )
255 })
256 }
257
258 pub fn ui_url(&self) -> String {
259 self.ui_url.clone().unwrap_or_else(|| {
260 self.base_url.clone().map_or_else(
261 || "https://vault.bitwarden.com".to_string(),
262 |url| {
263 let clean_url = url.trim_end_matches('/');
264 if clean_url == "https://api.bitwarden.eu" {
265 "https://vault.bitwarden.eu".to_string()
266 } else {
267 clean_url.to_string()
268 }
269 },
270 )
271 })
272 }
273
274 pub fn notifications_url(&self) -> String {
275 self.notifications_url.clone().unwrap_or_else(|| {
276 self.base_url.clone().map_or_else(
277 || "https://notifications.bitwarden.com".to_string(),
278 |url| {
279 let clean_url = url.trim_end_matches('/');
280 if clean_url == "https://api.bitwarden.eu" {
281 "https://notifications.bitwarden.eu".to_string()
282 } else {
283 format!("{clean_url}/notifications")
284 }
285 },
286 )
287 })
288 }
289
290 pub fn client_cert_path(&self) -> Option<&std::path::Path> {
291 self.client_cert_path.as_deref()
292 }
293
294 pub fn server_name(&self) -> String {
295 self.base_url
296 .clone()
297 .unwrap_or_else(|| "default".to_string())
298 }
299}
300
301pub async fn device_id(config: &Config) -> Result<String> {
302 let file = crate::dirs::device_id_file();
303 if let Ok(mut fh) = tokio::fs::File::open(&file).await {
304 let mut s = String::new();
305 fh.read_to_string(&mut s)
306 .await
307 .map_err(|e| Error::LoadDeviceId {
308 source: e,
309 file: file.clone(),
310 })?;
311 Ok(s.trim().to_string())
312 } else {
313 use std::os::unix::fs::PermissionsExt as _;
314 let id = config.device_id.as_ref().map_or_else(
315 || crate::uuid::new_v4().to_string(),
316 String::to_string,
317 );
318 let mut fh = tokio::fs::OpenOptions::new()
319 .write(true)
320 .create(true)
321 .truncate(true)
322 .mode(0o600)
323 .open(&file)
324 .await
325 .map_err(|e| Error::LoadDeviceId {
326 source: e,
327 file: file.clone(),
328 })?;
329 fh.set_permissions(std::fs::Permissions::from_mode(0o600))
333 .await
334 .map_err(|e| Error::LoadDeviceId {
335 source: e,
336 file: file.clone(),
337 })?;
338 fh.write_all(id.as_bytes()).await.map_err(|e| {
339 Error::LoadDeviceId {
340 source: e,
341 file: file.clone(),
342 }
343 })?;
344 Ok(id)
345 }
346}