authenticator/backup/
freeotp_json.rs

1use anyhow::Result;
2use gettextrs::gettext;
3use serde::Deserialize;
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6use super::{Restorable, RestorableItem};
7use crate::models::{Algorithm, Method};
8
9#[derive(Deserialize)]
10pub struct FreeOTPJSON {
11    tokens: Vec<FreeOTPItem>,
12}
13
14#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
15pub struct FreeOTPItem {
16    #[zeroize(skip)]
17    algo: Algorithm,
18    // Note: For some reason FreeOTP adds -1 to the counter
19    #[zeroize(skip)]
20    counter: Option<u32>,
21    #[zeroize(skip)]
22    digits: Option<u32>,
23    #[zeroize(skip)]
24    label: String,
25    #[serde(rename = "issuerExt")]
26    #[zeroize(skip)]
27    issuer: String,
28    #[zeroize(skip)]
29    period: Option<u32>,
30    secret: Vec<i16>,
31    #[serde(rename = "type")]
32    #[zeroize(skip)]
33    method: Method,
34}
35
36impl RestorableItem for FreeOTPItem {
37    fn account(&self) -> String {
38        self.label.clone()
39    }
40
41    fn issuer(&self) -> String {
42        self.issuer.clone()
43    }
44
45    fn secret(&self) -> String {
46        let secret = self
47            .secret
48            .iter()
49            .map(|x| (x & 0xff) as u8)
50            .collect::<Vec<_>>();
51        data_encoding::BASE32_NOPAD.encode(&secret)
52    }
53
54    fn period(&self) -> Option<u32> {
55        self.period
56    }
57
58    fn method(&self) -> Method {
59        self.method
60    }
61
62    fn algorithm(&self) -> Algorithm {
63        self.algo
64    }
65
66    fn digits(&self) -> Option<u32> {
67        self.digits
68    }
69
70    fn counter(&self) -> Option<u32> {
71        if self.method().is_event_based() {
72            // for some reason, FreeOTP adds -1 to the counter
73            self.counter.map(|c| c + 1)
74        } else {
75            None
76        }
77    }
78}
79
80impl Restorable for FreeOTPJSON {
81    const ENCRYPTABLE: bool = false;
82    const SCANNABLE: bool = false;
83    const IDENTIFIER: &'static str = "freeotp_json";
84    type Item = FreeOTPItem;
85
86    fn title() -> String {
87        gettext("FreeOTP+")
88    }
89
90    fn subtitle() -> String {
91        gettext("From a plain-text JSON file, compatible with FreeOTP+")
92    }
93
94    fn restore_from_data(from: &[u8], _key: Option<&str>) -> Result<Vec<Self::Item>> {
95        let root: FreeOTPJSON = serde_json::de::from_slice(from)?;
96        Ok(root.tokens)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn parse() {
106        let data = std::fs::read_to_string("./src/backup/tests/freeotp_json.json").unwrap();
107        let items = FreeOTPJSON::restore_from_data(data.as_bytes(), None).unwrap();
108
109        assert_eq!(items[0].account(), "Mason");
110        assert_eq!(items[0].issuer(), "Deno");
111        assert_eq!(items[0].secret(), "4SJHB4GSD43FZBAI7C2HLRJGPQ");
112        assert_eq!(items[0].period(), Some(30));
113        assert_eq!(items[0].method(), Method::TOTP);
114        assert_eq!(items[0].algorithm(), Algorithm::SHA1);
115        assert_eq!(items[0].digits(), Some(6));
116        assert_eq!(items[0].counter(), None);
117
118        assert_eq!(items[1].account(), "James");
119        assert_eq!(items[1].issuer(), "SPDX");
120        assert_eq!(items[1].secret(), "5OM4WOOGPLQEF6UGN3CPEOOLWU");
121        assert_eq!(items[1].period(), Some(20));
122        assert_eq!(items[1].method(), Method::TOTP);
123        assert_eq!(items[1].algorithm(), Algorithm::SHA256);
124        assert_eq!(items[1].digits(), Some(7));
125        assert_eq!(items[1].counter(), None);
126
127        assert_eq!(items[2].account(), "Elijah");
128        assert_eq!(items[2].issuer(), "Airbnb");
129        assert_eq!(items[2].secret(), "7ELGJSGXNCCTV3O6LKJWYFV2RA");
130        assert_eq!(items[2].period(), Some(50));
131        assert_eq!(items[2].method(), Method::TOTP);
132        assert_eq!(items[2].algorithm(), Algorithm::SHA512);
133        assert_eq!(items[2].digits(), Some(8));
134        assert_eq!(items[2].counter(), None);
135
136        assert_eq!(items[3].account(), "James");
137        assert_eq!(items[3].issuer(), "Issuu");
138        assert_eq!(items[3].secret(), "YOOMIXWS5GN6RTBPUFFWKTW5M4");
139        assert_eq!(items[3].period(), Some(30));
140        assert_eq!(items[3].method(), Method::HOTP);
141        assert_eq!(items[3].algorithm(), Algorithm::SHA1);
142        assert_eq!(items[3].digits(), Some(6));
143        assert_eq!(items[3].counter(), Some(1));
144
145        assert_eq!(items[4].account(), "Benjamin");
146        assert_eq!(items[4].issuer(), "Air Canada");
147        assert_eq!(items[4].secret(), "KUVJJOM753IHTNDSZVCNKL7GII");
148        assert_eq!(items[4].period(), Some(30));
149        assert_eq!(items[4].method(), Method::HOTP);
150        assert_eq!(items[4].algorithm(), Algorithm::SHA256);
151        assert_eq!(items[4].digits(), Some(7));
152        assert_eq!(items[4].counter(), Some(50));
153
154        assert_eq!(items[5].account(), "Mason");
155        assert_eq!(items[5].issuer(), "WWE");
156        assert_eq!(items[5].secret(), "5VAML3X35THCEBVRLV24CGBKOY");
157        assert_eq!(items[5].period(), Some(30));
158        assert_eq!(items[5].method(), Method::HOTP);
159        assert_eq!(items[5].algorithm(), Algorithm::SHA512);
160        assert_eq!(items[5].digits(), Some(8));
161        assert_eq!(items[5].counter(), Some(10300));
162    }
163}