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