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