1use hex;
3use secrecy::ExposeSecret;
4use serde::{Deserialize, Serialize};
5use sos_account::Account;
6use sos_backend::AccessPoint;
7use sos_core::VaultId;
8use sos_password::generator::measure_entropy;
9use sos_vault::{
10 secret::{Secret, SecretId, SecretType},
11 SecretAccess, Summary,
12};
13use zxcvbn::{Entropy, Score};
14
15pub async fn generate_security_report<A, E, T, D, R>(
17 account: &A,
18 options: SecurityReportOptions<T, D, R>,
19) -> Result<SecurityReport<T>, E>
20where
21 A: Account,
22 D: Fn(Vec<String>) -> R + Send + Sync,
23 R: std::future::Future<Output = Vec<T>> + Send + Sync,
24 E: From<A::Error>
25 + From<sos_account::Error>
26 + From<sos_core::Error>
27 + From<sos_vault::Error>
28 + From<sos_backend::Error>
29 + From<std::io::Error>
30 + From<sos_backend::StorageError>
31 + Send
32 + Sync
33 + 'static,
34{
35 let mut records = Vec::new();
36 let mut hashes = Vec::new();
37 let folders = account.list_folders().await?;
38 let targets: Vec<Summary> = folders
39 .into_iter()
40 .filter(|folder| {
41 if let Some(target) = &options.target {
42 return folder.id() == &target.0;
43 }
44 !options.excludes.contains(folder.id())
45 })
46 .collect();
47
48 for target in targets {
49 let folder = account.folder(target.id()).await?;
50 let access_point = folder.access_point();
51 let access_point = access_point.lock().await;
52
53 let vault = access_point.vault();
54 let mut password_hashes: Vec<(
55 SecretId,
56 (Option<Entropy>, Vec<u8>),
57 Option<SecretId>,
58 )> = Vec::new();
59
60 if let Some(target) = &options.target {
61 secret_security_report::<E>(
62 &target.1,
63 &*access_point,
64 &mut password_hashes,
65 target.2.as_ref(),
66 )
67 .await?;
68 } else {
69 for secret_id in vault.keys() {
70 secret_security_report::<E>(
71 secret_id,
72 &*access_point,
73 &mut password_hashes,
74 None,
75 )
76 .await?;
77 }
78 }
79
80 for (secret_id, check, field_id) in password_hashes {
81 let (entropy, sha1) = check;
82
83 let record = SecurityReportRecord {
84 folder: target.clone(),
85 secret_id,
86 field_id,
87 entropy,
88 };
89
90 hashes.push(hex::encode(sha1));
91 records.push(record);
92 }
93 }
94
95 let database_checks =
96 if let Some(database_handler) = options.database_handler {
97 (database_handler)(hashes).await
98 } else {
99 vec![]
100 };
101 Ok(SecurityReport {
102 records,
103 database_checks,
104 })
105}
106
107pub struct SecurityReportTarget(
109 pub VaultId,
110 pub SecretId,
111 pub Option<SecretId>,
112);
113
114pub struct SecurityReportOptions<T, H, F>
116where
117 H: Fn(Vec<String>) -> F,
118 F: std::future::Future<Output = Vec<T>>,
119{
120 pub excludes: Vec<VaultId>,
122 pub database_handler: Option<H>,
129
130 pub target: Option<SecurityReportTarget>,
140}
141
142#[derive(Debug, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct SecurityReportRow<T> {
146 pub folder_name: String,
148 pub folder_id: VaultId,
150 pub secret_id: SecretId,
152 pub field_id: Option<SecretId>,
154 pub score: Score,
156 pub guesses: u64,
158 pub guesses_log10: f64,
160 #[serde(rename = "breached")]
162 pub database_check: T,
163}
164
165impl SecurityReportRow<bool> {
166 pub fn is_secure(&self) -> bool {
173 self.score >= Score::Three && !self.database_check
174 }
175
176 pub fn is_insecure(&self) -> bool {
178 !self.is_secure()
179 }
180}
181
182pub struct SecurityReport<T> {
184 pub records: Vec<SecurityReportRecord>,
186 pub database_checks: Vec<T>,
188}
189
190impl<T> From<SecurityReport<T>> for Vec<SecurityReportRow<T>> {
191 fn from(value: SecurityReport<T>) -> Self {
192 let mut out = Vec::new();
193 for (record, database_check) in value
194 .records
195 .into_iter()
196 .zip(value.database_checks.into_iter())
197 {
198 let score = record
199 .entropy
200 .as_ref()
201 .map(|e| e.score())
202 .unwrap_or(Score::Zero);
203 let guesses =
204 record.entropy.as_ref().map(|e| e.guesses()).unwrap_or(0);
205 let guesses_log10 = record
206 .entropy
207 .as_ref()
208 .map(|e| e.guesses_log10())
209 .unwrap_or(0.0);
210
211 out.push(SecurityReportRow {
212 folder_name: record.folder.name().to_owned(),
213 folder_id: *record.folder.id(),
214 secret_id: record.secret_id,
215 field_id: record.field_id,
216 score,
217 guesses,
218 guesses_log10,
219 database_check,
220 });
221 }
222 out
223 }
224}
225
226pub struct SecurityReportRecord {
228 pub folder: Summary,
230 pub secret_id: SecretId,
232 pub field_id: Option<SecretId>,
234 pub entropy: Option<Entropy>,
238}
239
240async fn secret_security_report<E>(
241 secret_id: &SecretId,
242 access_point: &AccessPoint,
243 password_hashes: &mut Vec<(
244 SecretId,
245 (Option<Entropy>, Vec<u8>),
246 Option<SecretId>,
247 )>,
248 target_field: Option<&SecretId>,
249) -> Result<(), E>
250where
251 E: From<sos_vault::Error>
252 + From<sos_backend::StorageError>
253 + From<sos_backend::Error>,
254{
255 if let Some((_meta, secret, _)) =
256 access_point.read_secret(secret_id).await?
257 {
258 for field in secret.user_data().fields().iter().filter(|field| {
259 if let Some(field_id) = target_field {
260 return field_id == field.id();
261 }
262 true
263 }) {
264 if field.meta().kind() == &SecretType::Account
265 || field.meta().kind() == &SecretType::Password
266 {
267 let check = check_password::<E>(field.secret())?;
268 if let Some(check) = check {
269 password_hashes.push((
270 *secret_id,
271 check,
272 Some(*field.id()),
273 ));
274 }
275 }
276 }
277 let check = check_password::<E>(&secret)?;
278 if let Some(check) = check {
279 password_hashes.push((*secret_id, check, None));
280 }
281 }
282 Ok(())
283}
284
285pub fn check_password<E>(
290 secret: &Secret,
291) -> Result<Option<(Option<Entropy>, Vec<u8>)>, E>
292where
293 E: From<sos_vault::Error> + From<sos_backend::StorageError>,
294{
295 use sha1::{Digest, Sha1};
297 match secret {
298 Secret::Account {
299 account, password, ..
300 } => {
301 let hash = Sha1::digest(password.expose_secret().as_bytes());
302
303 if password.expose_secret().is_empty() {
306 Ok(Some((None, hash.to_vec())))
307 } else {
308 let entropy =
309 measure_entropy(password.expose_secret(), &[account]);
310 Ok(Some((Some(entropy), hash.to_vec())))
311 }
312 }
313 Secret::Password { password, name, .. } => {
314 let inputs = if let Some(name) = name {
315 vec![&name.expose_secret()[..]]
316 } else {
317 vec![]
318 };
319
320 let hash = Sha1::digest(password.expose_secret().as_bytes());
321
322 if password.expose_secret().is_empty() {
325 Ok(Some((None, hash.to_vec())))
326 } else {
327 let entropy = measure_entropy(
328 password.expose_secret(),
329 inputs.as_slice(),
330 );
331
332 Ok(Some((Some(entropy), hash.to_vec())))
333 }
334 }
335 _ => Ok(None),
336 }
337}