authenticator/backup/
freeotp_json.rs1use 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 #[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 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}