lib2fas/lib2fas.rs
1use crate::decrypt::decrypt_services;
2use crate::fuzzy::fuzzy_match;
3use crate::helpers::{calculate_hash, now};
4use crate::json5_support::{json5_from_reader, json5_from_reader_async};
5use core::fmt::Formatter;
6use core::hash::{Hash, Hasher};
7use serde::{Deserialize, Serialize, Serializer};
8use std::collections::{BTreeMap, VecDeque};
9use std::path::PathBuf;
10
11#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
12pub(crate) struct RawOTPDetails {
13 link: Option<String>,
14 #[serde(alias = "tokenType")]
15 token_type: Option<String>,
16 source: Option<String>,
17 label: Option<String>,
18 account: Option<String>,
19 digits: Option<i32>,
20 period: Option<i32>,
21}
22
23impl RawOTPDetails {
24 #[must_use]
25 pub fn totp_client(&self) -> Option<totp_rs::TOTP> {
26 let link = self.link.as_ref()?;
27
28 // TOTP::from_url checks the secret length for some reason, this version does not:
29 totp_rs::TOTP::from_url_unchecked(link).ok()
30 }
31}
32
33/// Represents details of a one-time password (OTP) entry, including metadata, configuration,
34/// and a TOTP client for generating time-based OTPs.
35///
36/// This struct is used to encapsulate the data and functionality associated with OTPs, including:
37/// - TOTP generation at the current time, previous, or future time intervals.
38/// - Conversion between string and numeric representations of tokens.
39#[derive(Serialize, Debug, Clone, Default)]
40pub struct OTPDetails {
41 /// The link associated with the OTP, typically containing configuration details in URL format.
42 link: Option<String>,
43 /// The type of the token (e.g., TOTP, HOTP).
44 token_type: Option<String>,
45 /// The source or origin of the OTP configuration.
46 source: Option<String>,
47 /// A human-readable label for the OTP entry.
48 label: Option<String>,
49 /// The account associated with this OTP.
50 account: Option<String>,
51 /// The number of digits in the generated OTP (e.g., 6 or 8).
52 digits: Option<i32>,
53 /// The period (in seconds) for generating time-based OTPs.
54 period: Option<i32>,
55 /// A TOTP client capable of generating time-based OTPs.
56 #[serde(skip_serializing)]
57 client: Option<totp_rs::TOTP>,
58}
59
60impl PartialEq for OTPDetails {
61 fn eq(
62 &self,
63 other: &Self,
64 ) -> bool {
65 // this ensures 'client' is omitted from eq check while other fields are checked
66 self.fields().eq(&other.fields())
67 }
68}
69
70// empty impl but required to make Rust understand that eq is possible based on previous partialeq
71impl Eq for OTPDetails {}
72
73impl Hash for OTPDetails {
74 fn hash<H: Hasher>(
75 &self,
76 state: &mut H,
77 ) {
78 self.fields().hash(state);
79 // exclude client!
80 }
81}
82
83impl From<RawOTPDetails> for OTPDetails {
84 fn from(val: RawOTPDetails) -> Self {
85 let client = val.totp_client();
86
87 Self {
88 client,
89 link: val.link,
90 token_type: val.token_type,
91 source: val.source,
92 label: val.label,
93 account: val.account,
94 digits: val.digits,
95 period: val.period,
96 }
97 }
98}
99
100impl OTPDetails {
101 /// Generates the current time-based OTP as a string, if possible.
102 ///
103 /// # Returns
104 /// - `Some(String)`: The OTP generated at the current time.
105 /// - `None`: If the TOTP client or current timestamp is unavailable.
106 #[must_use]
107 pub fn totp(&self) -> Option<String> {
108 self.totp_at(now()?)
109 }
110
111 /// Generates the current time-based OTP as a 32-bit unsigned integer, if possible.
112 ///
113 /// # Returns
114 /// - `Some(u32)`: The OTP generated at the current time as a number.
115 /// - `None`: If the TOTP client or current timestamp is unavailable.
116 #[must_use]
117 pub fn totp_u32(&self) -> Option<u32> {
118 let token = self.totp()?;
119
120 token.parse().ok()
121 }
122
123 /// Generates the next time-based OTP as a string, if possible.
124 ///
125 /// The next OTP corresponds to the token generated for the next time interval.
126 ///
127 /// # Returns
128 /// - `Some(String)`: The next OTP.
129 /// - `None`: If the TOTP client or current timestamp is unavailable.
130 #[must_use]
131 pub fn totp_next(&self) -> Option<String> {
132 let next_t = now()? + 30; // new token every 30s
133 self.totp_at(next_t)
134 }
135
136 /// Generates the next time-based OTP as a 32-bit unsigned integer, if possible.
137 ///
138 /// # Returns
139 /// - `Some(u32)`: The next OTP as a number.
140 /// - `None`: If the TOTP client or current timestamp is unavailable.
141 #[must_use]
142 pub fn totp_next_u32(&self) -> Option<u32> {
143 let token = self.totp_next()?;
144 token.parse().ok()
145 }
146
147 /// Generates the previous time-based OTP as a string, if possible.
148 ///
149 /// The previous OTP corresponds to the token generated for the last time interval.
150 ///
151 /// # Returns
152 /// - `Some(String)`: The previous OTP.
153 /// - `None`: If the TOTP client or current timestamp is unavailable.
154 #[must_use]
155 pub fn totp_previous(&self) -> Option<String> {
156 let prev_t = now()? - 30; // new token every 30s
157 self.totp_at(prev_t)
158 }
159
160 /// Generates the previous time-based OTP as a 32-bit unsigned integer, if possible.
161 ///
162 /// # Returns
163 /// - `Some(u32)`: The previous OTP as a number.
164 /// - `None`: If the TOTP client or current timestamp is unavailable.
165 #[must_use]
166 pub fn totp_previous_u32(&self) -> Option<u32> {
167 let token = self.totp_previous()?;
168 token.parse().ok()
169 }
170
171 /// Generates a time-based OTP for a specific timestamp, if possible.
172 ///
173 /// This method allows generating OTPs for custom timestamps, useful for testing or debugging.
174 ///
175 /// # Parameters
176 /// - `timestamp_sec`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
177 ///
178 /// # Returns
179 /// - `Some(String)`: The OTP for the given timestamp.
180 /// - `None`: If the TOTP client is unavailable.
181 #[must_use]
182 pub fn totp_at(
183 &self,
184 timestamp_sec: u64,
185 ) -> Option<String> {
186 self.client
187 .as_ref()
188 .map(|client| client.generate(timestamp_sec))
189 }
190
191 /// Generates a time-based OTP for a specific timestamp as a 32-bit unsigned integer, if possible.
192 ///
193 /// # Parameters
194 /// - `timestamp_sec`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
195 ///
196 /// # Returns
197 /// - `Some(u32)`: The OTP for the given timestamp as a number.
198 /// - `None`: If the TOTP client is unavailable.
199 #[must_use]
200 pub fn totp_at_u32(
201 &self,
202 timestamp_sec: u64,
203 ) -> Option<u32> {
204 let token = self.totp_at(timestamp_sec)?;
205 token.parse().ok()
206 }
207
208 #[expect(
209 clippy::type_complexity,
210 reason = "It's a weird type but that's okay for this internal method"
211 )]
212 const fn fields(
213 &self
214 ) -> (
215 &Option<String>,
216 &Option<String>,
217 &Option<String>,
218 &Option<String>,
219 &Option<String>,
220 &Option<i32>,
221 &Option<i32>,
222 ) {
223 // internal function to track fields used in hash, eq
224 (
225 &self.link,
226 &self.token_type,
227 &self.source,
228 &self.label,
229 &self.account,
230 &self.digits,
231 &self.period,
232 )
233 }
234}
235
236/// Represents a unique ordering of services with a positional value.
237///
238/// Typically used to define display or priority order.
239#[derive(
240 Serialize, Deserialize, Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default,
241)]
242pub struct OrderDetails {
243 position: u32,
244}
245
246/// Details of an icon collection associated with a service, identified by a unique ID.
247#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
248pub struct IconCollectionDetails {
249 id: String,
250}
251
252/// Represents icon details for a service, including the selected icon and its collection.
253#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
254pub struct IconDetails {
255 selected: String,
256 #[serde(alias = "iconCollection")]
257 icon_collection: IconCollectionDetails,
258}
259
260#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
261pub(crate) struct RawTwoFactorAuthDetails {
262 /// `unique_id` = hash of name+secret, calculated after deserializing
263 pub name: String,
264 secret: String,
265 #[serde(alias = "updatedAt")]
266 pub updated_at: u32,
267 #[serde(alias = "serviceTypeId")]
268 pub service_type_id: Option<String>,
269 pub otp: Option<RawOTPDetails>,
270 pub order: Option<OrderDetails>,
271 pub icon: Option<IconDetails>,
272 #[serde(alias = "groupId")]
273 pub group_id: Option<String>,
274}
275
276/// Represents a parsed and structured two-factor authentication (2FA) service.
277///
278/// This struct encapsulates all details and functionality associated with a single 2FA service, including:
279/// - Metadata such as name, updated time, and associated group.
280/// - Configuration details for generating OTPs.
281/// - Integration with a TOTP client for token generation.
282///
283/// `TwoFactorAuthDetails` provides methods to generate OTPs and query associated metadata.
284///
285/// # Example
286/// ```rust
287/// use lib2fas::load_services;
288/// use lib2fas::TwoFactorAuthDetails;
289///
290/// #[tokio::main]
291/// async fn main() {
292/// let result = load_services("path/to/services.2fas", Some("passphrase")).await;
293/// match result {
294/// Ok(storage) => {
295/// if let Some(service) = storage.first() {
296/// // service: TwoFactorAuthDetails
297/// println!("Service Name: {}", service.name);
298/// if let Some(otp) = service.totp() {
299/// println!("Current OTP: {}", otp);
300/// } else {
301/// println!("OTP generation not available for this service.");
302/// }
303/// } else {
304/// println!("No services found.");
305/// }
306/// },
307/// Err(err) => {
308/// eprintln!("Failed to load services: {}", err);
309/// },
310/// }
311/// }
312/// ```
313#[derive(Serialize, Debug, Clone, PartialEq, Eq, Default, Hash)]
314pub struct TwoFactorAuthDetails {
315 /// A unique identifier for the 2FA service, derived from its name and secret.
316 #[serde(serialize_with = "serialize_to_string")]
317 pub unique_id: u64,
318 /// The name of the 2FA service.
319 pub name: String,
320 /// The secret used for generating OTPs.
321 secret: String,
322 /// The timestamp (in seconds since the Unix epoch) when the service was last updated.
323 pub updated_at: u32,
324 /// The type of the service, if applicable.
325 pub service_type_id: Option<String>,
326 /// The OTP details associated with the service, including configuration and TOTP client.
327 pub otp: Option<OTPDetails>,
328 /// The order details of the service, used for display or prioritization.
329 pub order: Option<OrderDetails>,
330 /// The icon details of the service, including its collection and selected icon.
331 pub icon: Option<IconDetails>,
332 /// The ID of the group this service belongs to, if any.
333 pub group_id: Option<String>,
334}
335
336impl TwoFactorAuthDetails {
337 /// Generates the current time-based OTP as a string, if possible.
338 ///
339 /// # Returns
340 /// - `Some(String)`: The OTP generated at the current time.
341 /// - `None`: If OTP details or the TOTP client are unavailable.
342 #[must_use]
343 pub fn totp(&self) -> Option<String> {
344 self.otp.as_ref().and_then(OTPDetails::totp)
345 }
346
347 /// Generates the current time-based OTP as a 32-bit unsigned integer, if possible.
348 ///
349 /// # Returns
350 /// - `Some(u32)`: The OTP generated at the current time as a number.
351 /// - `None`: If OTP details or the TOTP client are unavailable.
352 #[must_use]
353 pub fn totp_u32(&self) -> Option<u32> {
354 self.otp.as_ref().and_then(OTPDetails::totp_u32)
355 }
356
357 /// Generates the next time-based OTP as a string, if possible.
358 ///
359 /// The next OTP corresponds to the token generated for the next time interval.
360 ///
361 /// # Returns
362 /// - `Some(String)`: The next OTP.
363 /// - `None`: If OTP details or the TOTP client are unavailable.
364 #[cfg(not(tarpaulin))]
365 #[must_use]
366 pub fn totp_next(&self) -> Option<String> {
367 self.otp.as_ref().and_then(OTPDetails::totp_next)
368 }
369
370 /// Generates the next time-based OTP as a 32-bit unsigned integer, if possible.
371 ///
372 /// # Returns
373 /// - `Some(u32)`: The next OTP as a number.
374 /// - `None`: If OTP details or the TOTP client are unavailable.
375 #[must_use]
376 pub fn totp_next_u32(&self) -> Option<u32> {
377 self.otp.as_ref().and_then(OTPDetails::totp_next_u32)
378 }
379
380 /// Generates the previous time-based OTP as a string, if possible.
381 ///
382 /// The previous OTP corresponds to the token generated for the last time interval.
383 ///
384 /// # Returns
385 /// - `Some(String)`: The previous OTP.
386 /// - `None`: If OTP details or the TOTP client are unavailable.
387 #[cfg(not(tarpaulin))]
388 #[must_use]
389 pub fn totp_previous(&self) -> Option<String> {
390 self.otp.as_ref().and_then(OTPDetails::totp_previous)
391 }
392
393 /// Generates the previous time-based OTP as a 32-bit unsigned integer, if possible.
394 ///
395 /// # Returns
396 /// - `Some(u32)`: The previous OTP as a number.
397 /// - `None`: If OTP details or the TOTP client are unavailable.
398 #[must_use]
399 pub fn totp_previous_u32(&self) -> Option<u32> {
400 self.otp.as_ref().and_then(OTPDetails::totp_previous_u32)
401 }
402
403 /// Generates a time-based OTP for a specific timestamp, if possible.
404 ///
405 /// This method allows generating OTPs for custom timestamps, useful for testing or debugging.
406 ///
407 /// # Parameters
408 /// - `time`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
409 ///
410 /// # Returns
411 /// - `Some(String)`: The OTP for the given timestamp.
412 /// - `None`: If OTP details or the TOTP client are unavailable.
413 #[cfg(not(tarpaulin))]
414 #[must_use]
415 pub fn totp_at(
416 &self,
417 time: u64,
418 ) -> Option<String> {
419 self.otp.as_ref().and_then(|client| client.totp_at(time))
420 }
421
422 /// Generates a time-based OTP for a specific timestamp as a 32-bit unsigned integer, if possible.
423 ///
424 /// # Parameters
425 /// - `time`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
426 ///
427 /// # Returns
428 /// - `Some(u32)`: The OTP for the given timestamp as a number.
429 /// - `None`: If OTP details or the TOTP client are unavailable.
430 #[must_use]
431 pub fn totp_at_u32(
432 &self,
433 time: u64,
434 ) -> Option<u32> {
435 self.otp
436 .as_ref()
437 .and_then(|client| client.totp_at_u32(time))
438 }
439}
440
441impl From<RawTwoFactorAuthDetails> for TwoFactorAuthDetails {
442 fn from(val: RawTwoFactorAuthDetails) -> Self {
443 let unique_id = calculate_hash(&format!("{}:{}", val.name, val.secret));
444 let otp_with_client = val.otp.map(Into::into);
445
446 Self {
447 unique_id,
448 name: val.name,
449 secret: val.secret,
450 updated_at: val.updated_at,
451 service_type_id: val.service_type_id,
452 otp: otp_with_client,
453 order: val.order,
454 icon: val.icon,
455 group_id: val.group_id,
456 }
457 }
458}
459
460/// Represents a group of services, including metadata and expanded state.
461#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
462pub struct Group {
463 id: String,
464 name: String,
465
466 #[serde(alias = "isExpanded")]
467 is_expanded: bool,
468
469 #[serde(alias = "updatedAt")]
470 updated_at: u32,
471}
472
473#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
474pub(crate) struct RawTwoFactorStorage {
475 services: Vec<RawTwoFactorAuthDetails>,
476
477 groups: Option<Vec<Group>>,
478
479 #[serde(alias = "updatedAt")]
480 updated_at: Option<u32>,
481
482 #[serde(alias = "schemaVersion")]
483 schema_version: Option<u8>, // should warn if not 4
484
485 #[serde(alias = "appVersionCode")]
486 app_version_code: Option<i32>,
487
488 #[serde(alias = "appVersionName")]
489 app_version_name: Option<String>,
490
491 #[serde(alias = "appOrigin")]
492 app_origin: Option<String>,
493
494 #[serde(alias = "servicesEncrypted")]
495 services_encrypted: Option<String>,
496}
497
498impl RawTwoFactorStorage {
499 pub fn is_encrypted(&self) -> bool {
500 self.services_encrypted
501 .as_ref()
502 .is_some_and(|value| !value.is_empty())
503 }
504
505 pub fn decrypt(
506 &mut self,
507 passphrase: &str,
508 ) -> anyhow::Result<()> {
509 if let Some(data) = &self.services_encrypted {
510 let entries = decrypt_services(data, passphrase)?;
511
512 self.services.extend(entries);
513 self.services_encrypted = None;
514 }
515
516 Ok(())
517 }
518
519 fn try_into_final(
520 mut self,
521 maybe_passphrase: Option<&str>,
522 ) -> TwofasResult {
523 if self.is_encrypted() {
524 if let Some(passphrase) = maybe_passphrase {
525 self.decrypt(passphrase)?;
526 } else {
527 return Err(TwofasError::FileEncrypted);
528 }
529 }
530
531 Ok(self.into())
532 }
533}
534
535type ServicesByName = BTreeMap<String, Vec<RawTwoFactorAuthDetails>>;
536
537#[must_use]
538fn collect_services_by_name(services: &[RawTwoFactorAuthDetails]) -> ServicesByName {
539 let mut services_by_name: ServicesByName = BTreeMap::new();
540
541 for service in services.iter().cloned() {
542 services_by_name
543 .entry(service.name.clone())
544 .or_default()
545 .push(service);
546 }
547
548 services_by_name
549}
550
551type ServicesById = BTreeMap<u64, TwoFactorAuthDetails>;
552
553#[must_use]
554fn collect_services_by_id(services: &Vec<RawTwoFactorAuthDetails>) -> ServicesById {
555 let mut service_by_id: ServicesById = BTreeMap::new();
556 for service_raw in services {
557 let service: TwoFactorAuthDetails = service_raw.clone().into();
558 service_by_id.entry(service.unique_id).or_insert(service);
559 }
560
561 service_by_id
562}
563
564impl From<RawTwoFactorStorage> for TwoFactorStorage {
565 /// assumes it's already decrypted. Otherwise, use `try_into_final`
566 fn from(val: RawTwoFactorStorage) -> Self {
567 let by_name = collect_services_by_name(&val.services);
568 let by_id = collect_services_by_id(&val.services);
569 let count = by_id.len();
570
571 Self {
572 services: by_id,
573 services_raw: by_name,
574 count,
575
576 groups: val.groups,
577 updated_at: val.updated_at,
578
579 schema_version: val.schema_version,
580 app_version_code: val.app_version_code,
581 app_version_name: val.app_version_name,
582 app_origin: val.app_origin,
583 }
584 }
585}
586
587// fn btree_keys_only<S: Serializer, K: Serialize, V>(
588// input: &BTreeMap<K, V>,
589// serializer: S,
590// ) -> Result<S::Ok, S::Error> {
591// let values: Vec<&K> = input.keys().collect();
592// values.serialize(serializer)
593// }
594
595/// Usage: `#[serde(serialize_with = "btree_values_only")]`
596fn btree_values_only<S: Serializer, K, V: Serialize>(
597 input: &BTreeMap<K, V>,
598 serializer: S,
599) -> Result<S::Ok, S::Error> {
600 let values: Vec<&V> = input.values().collect();
601 values.serialize(serializer)
602}
603
604/// Usage: `#[serde(serialize_with = "serialize_to_string")]`
605fn serialize_to_string<S: Serializer, T: ToString>(
606 input: &T,
607 serializer: S,
608) -> Result<S::Ok, S::Error> {
609 let value = input.to_string();
610 value.serialize(serializer)
611}
612
613/// Represents a storage container for two-factor authentication (2FA) services.
614///
615/// This struct provides an organized structure for managing 2FA services and associated metadata.
616/// It includes methods for querying services by ID or name, iterating over stored services, and
617/// serializing or deserializing the data.
618///
619/// # Example
620/// ```rust
621/// use lib2fas::{load_services};
622/// use lib2fas::TwoFactorStorage;
623///
624/// #[tokio::main]
625/// async fn main() {
626/// let result = load_services("path/to/services.2fas", Some("passphrase")).await;
627/// match result {
628/// Ok(storage) => {
629/// // storage: TwoFactorStorage
630/// println!("Loaded {} services", storage.len());
631///
632/// if let Some(service) = storage.first() {
633/// println!("First service: {}", service.name);
634/// }
635///
636/// let services_named = storage.by_name("example");
637/// println!("Found {} services with the name 'example'", services_named.len());
638/// },
639/// Err(err) => {
640/// eprintln!("Failed to load services: {}", err);
641/// },
642/// }
643/// }
644/// ```
645#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
646pub struct TwoFactorStorage {
647 /// A map of services indexed by their unique ID.
648 #[serde(serialize_with = "btree_values_only")]
649 services: BTreeMap<u64, TwoFactorAuthDetails>,
650 /// A map of raw services indexed by their name.
651 #[serde(skip_serializing)]
652 services_raw: BTreeMap<String, Vec<RawTwoFactorAuthDetails>>,
653 /// An optional list of groups associated with the services.
654 groups: Option<Vec<Group>>,
655 /// The timestamp (in seconds since the Unix epoch) when the storage was last updated.
656 updated_at: Option<u32>,
657 /// The schema version of the stored data.
658 schema_version: Option<u8>,
659 /// The version code of the application that created the storage.
660 app_version_code: Option<i32>,
661 /// The version name of the application that created the storage.
662 app_version_name: Option<String>,
663 /// The origin or source of the application that created the storage.
664 app_origin: Option<String>,
665 /// The number of services stored.
666 count: usize,
667}
668
669impl TwoFactorStorage {
670 /// Checks whether the storage is empty.
671 ///
672 /// # Returns
673 /// - `true` if there are no services stored.
674 /// - `false` otherwise.
675 #[must_use]
676 pub fn is_empty(&self) -> bool {
677 self.services.is_empty()
678 }
679
680 /// Retrieves the first service in the storage.
681 ///
682 /// # Returns
683 /// - `Some(&TwoFactorAuthDetails)` if at least one service exists.
684 /// - `None` if the storage is empty.
685 #[must_use]
686 pub fn first(&self) -> Option<&TwoFactorAuthDetails> {
687 Some(self.services.first_key_value()?.1)
688 }
689
690 /// Retrieves a service by its unique ID.
691 ///
692 /// # Parameters
693 /// - `id`: The unique ID of the service to retrieve.
694 ///
695 /// # Returns
696 /// - `Some(&TwoFactorAuthDetails)` if the service exists.
697 /// - `None` if no service is found with the given ID.
698 #[must_use]
699 pub fn by_id(
700 &self,
701 id: u64,
702 ) -> Option<&TwoFactorAuthDetails> {
703 self.services.get(&id)
704 }
705
706 /// Retrieves all services with the specified name.
707 ///
708 /// # Parameters
709 /// - `name`: The name of the services to retrieve.
710 ///
711 /// # Returns
712 /// - A vector of `TwoFactorAuthDetails` matching the given name.
713 #[must_use]
714 pub fn by_name(
715 &self,
716 name: &str,
717 ) -> Vec<TwoFactorAuthDetails> {
718 static DEFAULT_VEC: Vec<RawTwoFactorAuthDetails> = vec![];
719 self.services_raw
720 .get(name)
721 .unwrap_or(&DEFAULT_VEC)
722 .iter()
723 .map(|it| it.clone().into())
724 .collect()
725 }
726
727 fn get(
728 &self,
729 key: &str,
730 ) -> Option<&Vec<RawTwoFactorAuthDetails>> {
731 // old, use by_id or by_name instead
732 self.services_raw.get(key)
733 }
734
735 /// Retrieves the first service with the specified name.
736 ///
737 /// # Parameters
738 /// - `name`: The name of the service to retrieve.
739 ///
740 /// # Returns
741 /// - `Some(TwoFactorAuthDetails)` if a service with the given name exists.
742 /// - `None` if no service is found with the given name.
743 #[must_use]
744 pub fn first_by_name(
745 &self,
746 name: &str,
747 ) -> Option<TwoFactorAuthDetails> {
748 self.by_name(name).first().cloned()
749 }
750
751 /// Searches for services matching the specified query string.
752 ///
753 /// This method performs a fuzzy match on the service names.
754 ///
755 /// # Parameters
756 /// - `query`: The query string to search for.
757 ///
758 /// # Returns
759 /// - A new `TwoFactorStorage` containing only the matching services.
760 #[must_use]
761 pub fn find(
762 &self,
763 query: &str,
764 ) -> Self {
765 let services_by_name: BTreeMap<String, Vec<RawTwoFactorAuthDetails>>;
766 let services_by_id: BTreeMap<u64, TwoFactorAuthDetails>;
767
768 let mut count: usize = 0;
769
770 if let Some(vec) = self.get(query) {
771 services_by_name = BTreeMap::from([(query.to_owned(), vec.to_owned())]);
772 services_by_id = collect_services_by_id(vec);
773
774 count = vec.len();
775 } else {
776 // todo: if no services was found in key, look in values? (e.g. description/notes etc)
777 let matching_services: Vec<_> = self
778 .services_raw
779 .clone()
780 .into_iter()
781 .filter_map(|(key, values)| {
782 fuzzy_match(query, key).then(|| {
783 count += values.len();
784 values
785 })
786 })
787 .flatten()
788 .collect();
789 services_by_name = collect_services_by_name(&matching_services);
790 services_by_id = collect_services_by_id(&matching_services);
791 }
792
793 Self {
794 services: services_by_id,
795 services_raw: services_by_name,
796 count,
797
798 ..self.clone()
799 }
800 }
801
802 /// Retrieves the first service matching the specified query string.
803 ///
804 /// This method performs a fuzzy match on the service names.
805 ///
806 /// # Parameters
807 /// - `query`: The query string to search for.
808 ///
809 /// # Returns
810 /// - `Some(TwoFactorAuthDetails)` if a matching service is found.
811 /// - `None` if no service matches the query.
812 #[must_use]
813 pub fn find_first(
814 &self,
815 name: &str,
816 ) -> Option<TwoFactorAuthDetails> {
817 self.find(name).first().cloned()
818 }
819
820 /// Returns the total number of services stored.
821 ///
822 /// # Returns
823 /// - The number of services in the storage.
824 #[must_use]
825 pub const fn len(&self) -> usize {
826 self.count
827 }
828
829 /// Returns an iterator over the stored services.
830 ///
831 /// # Returns
832 /// - An iterator yielding references to `TwoFactorAuthDetails`.
833 #[must_use]
834 pub fn iter(&self) -> TwoFactorIterator<&TwoFactorAuthDetails> {
835 let values: VecDeque<_> = self.services.values().collect();
836 TwoFactorIterator { values }
837 }
838
839 /// Deserializes a `TwoFactorStorage` instance from a JSON string.
840 ///
841 /// # Parameters
842 /// - `json`: The JSON string containing the serialized storage data.
843 ///
844 /// # Returns
845 /// - `Some(Self)` if deserialization is successful.
846 /// - `TwofasError` if the deserialization fails.
847 pub fn try_from_json(json: &str) -> TwofasResult {
848 let raw: RawTwoFactorStorage = json5::from_str(json)?;
849 raw.try_into_final(None)
850 }
851
852 /// Deserializes a `TwoFactorStorage` instance from a JSON string.
853 ///
854 /// # Parameters
855 /// - `json`: The JSON string containing the serialized storage data.
856 ///
857 /// # Returns
858 /// - `Some(Self)` if deserialization is successful.
859 /// - `None` if the deserialization fails.
860 #[must_use]
861 pub fn from_json(json: &str) -> Option<Self> {
862 let result: RawTwoFactorStorage = json5::from_str(json).ok()?;
863 result.try_into_final(None).ok()
864 }
865
866 /// Serializes the storage to a JSON string.
867 ///
868 /// # Returns
869 /// - `Some(String)` containing the serialized JSON representation.
870 /// - `None` if serialization fails.
871 #[must_use]
872 pub fn to_json(&self) -> Option<String> {
873 json5::to_string(self).ok()
874 }
875}
876
877/// Iterator for two-factor authentication services.
878///
879/// This struct provides an iterator for traversing the services stored in a `TwoFactorStorage` instance.
880///
881/// # Example
882/// ```rust
883/// use lib2fas::{load_services, TwoFactorStorage};
884///
885/// #[tokio::main]
886/// async fn main() {
887/// let result = load_services("path/to/services.2fas", Some("passphrase")).await;
888/// if let Ok(storage) = result {
889/// for service in storage.iter() {
890/// println!("Service Name: {}", service.name);
891/// }
892/// }
893/// }
894/// ```
895pub struct TwoFactorIterator<T> {
896 values: VecDeque<T>,
897}
898
899impl<T> Iterator for TwoFactorIterator<T> {
900 type Item = T;
901
902 fn next(&mut self) -> Option<Self::Item> {
903 self.values.pop_front()
904 }
905}
906
907impl IntoIterator for TwoFactorStorage {
908 type Item = TwoFactorAuthDetails;
909 type IntoIter = TwoFactorIterator<Self::Item>;
910
911 fn into_iter(self) -> Self::IntoIter {
912 let values: VecDeque<_> = self.services.into_values().collect();
913 Self::IntoIter { values }
914 }
915}
916
917impl<'tfs> IntoIterator for &'tfs TwoFactorStorage {
918 type Item = &'tfs TwoFactorAuthDetails;
919 type IntoIter = TwoFactorIterator<&'tfs TwoFactorAuthDetails>;
920 fn into_iter(self) -> Self::IntoIter {
921 self.iter()
922 }
923}
924
925/// Errors that can occur while using the `lib2fas` library.
926#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
927#[non_exhaustive]
928pub enum TwofasError {
929 /// Indicates that the file is encrypted but no passphrase was provided.
930 FileEncrypted,
931 /// Represents other errors with a description of the problem.
932 Other(String),
933}
934
935#[expect(
936 clippy::min_ident_chars,
937 reason = "compatibility with `std::fmt::Display.fmt`"
938)]
939impl core::fmt::Display for TwofasError {
940 fn fmt(
941 &self,
942 f: &mut Formatter<'_>,
943 ) -> core::fmt::Result {
944 match self {
945 Self::FileEncrypted => {
946 write!(f, "File is encrypted but no passphrase was provided!")
947 },
948 Self::Other(err) => {
949 write!(f, "{err}")
950 },
951 }
952 }
953}
954
955impl From<anyhow::Error> for TwofasError {
956 fn from(value: anyhow::Error) -> Self {
957 Self::Other(value.to_string())
958 }
959}
960
961impl From<std::io::Error> for TwofasError {
962 fn from(value: std::io::Error) -> Self {
963 Self::Other(format!("file read error: {value}"))
964 }
965}
966
967impl From<json5::Error> for TwofasError {
968 fn from(value: json5::Error) -> Self {
969 Self::Other(format!("json decode error: {value}"))
970 }
971}
972
973type TwofasResult = Result<TwoFactorStorage, TwofasError>;
974
975/// Loads and parses two-factor authentication services from a given 2fas file.
976///
977/// # Overview
978/// `load_services` serves as the main entry point for the `lib2fas` library. It reads a file containing
979/// two-factor authentication service details (potentially encrypted), parses its content, and
980/// returns a structured representation of the services.
981///
982/// If the file is encrypted, a passphrase must be provided to decrypt and load the data.
983///
984/// # Parameters
985/// - `filename`: The path to the 2fas file containing the two-factor authentication services.
986/// This can be any type that implements `Into<PathBuf>`.
987/// - `passphrase`: An optional passphrase used to decrypt the file if it's encrypted. If the file
988/// is encrypted and no passphrase is provided, an error will be returned.
989///
990/// # Returns
991/// - `Ok(TwoFactorStorage)`: A structured representation of the two-factor services, including
992/// service details, groups, metadata, and additional attributes.
993/// - `Err(TwofasError)`: An error indicating the reason for failure, such as file I/O issues,
994/// parsing errors, or missing decryption passphrase for encrypted files.
995///
996/// # Errors
997/// - Returns `TwofasError::FileEncrypted` if the file is encrypted but no passphrase is provided.
998/// - Returns `TwofasError::Other` for other issues, such as file read errors or invalid content.
999///
1000/// # Example
1001/// ```rust
1002/// async fn example() {
1003/// use lib2fas::load_services;
1004///
1005/// let result = load_services("path/to/services.2fas", Some("passphrase")).await;
1006/// match result {
1007/// Ok(storage) => {
1008/// println!("Loaded {} services", storage.len());
1009/// },
1010/// Err(err) => {
1011/// eprintln!("Failed to load services: {}", err);
1012/// },
1013/// }
1014/// }
1015/// ```
1016///
1017/// # See Also
1018/// The [`TwoFactorStorage`](struct.TwoFactorStorage.html) struct contains methods for interacting
1019/// with the loaded two-factor authentication services, such as querying by name or ID, and
1020/// iterating over the stored services.
1021///
1022/// # Note
1023/// This function is asynchronous and should be awaited to complete its execution.
1024/// You can use `load_services_blocking` if you can't use async.
1025pub async fn load_services<P: Into<PathBuf>>(
1026 filename: P,
1027 passphrase: Option<&str>,
1028) -> TwofasResult {
1029 use tokio::fs::File;
1030 use tokio::io::BufReader;
1031
1032 let file = File::open(filename.into())
1033 .await
1034 .map_err(TwofasError::from)?;
1035
1036 let reader = BufReader::new(file);
1037
1038 let result: RawTwoFactorStorage = json5_from_reader_async(reader).await?;
1039
1040 result.try_into_final(passphrase)
1041}
1042
1043/// Blocking variant of `load_services`.
1044pub fn load_services_blocking<P: Into<PathBuf>>(
1045 filename: P,
1046 passphrase: Option<&str>,
1047) -> TwofasResult {
1048 use std::fs::File;
1049 use std::io::BufReader;
1050
1051 let file = File::open(filename.into()).map_err(TwofasError::from)?;
1052 let reader = BufReader::new(file);
1053
1054 let result: RawTwoFactorStorage = json5_from_reader(reader)?;
1055
1056 result.try_into_final(passphrase)
1057}