1use anyhow::{Context, Result};
2use gettextrs::gettext;
3use gtk::prelude::*;
4use ring::{
5 aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey},
6 digest,
7};
8use serde::{Deserialize, Serialize};
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use super::{Backupable, Restorable, RestorableItem};
12use crate::models::{Account, Algorithm, Method, Provider, ProvidersModel};
13
14const HEADER_SIZE: usize = size_of::<EncryptedAndOTPHeader>();
15
16#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
17pub struct AndOTP(Vec<AndOTPItem>);
18
19#[derive(bincode::Decode)]
20pub struct EncryptedAndOTPHeader {
21 iterations: u32,
22 salt: [u8; 12],
23 iv: [u8; 12],
24}
25
26impl EncryptedAndOTPHeader {
27 fn from_bytes(from: &[u8]) -> Result<Self> {
28 let config = bincode::config::standard()
29 .with_fixed_int_encoding()
30 .with_limit::<HEADER_SIZE>()
31 .with_big_endian();
32
33 let header: EncryptedAndOTPHeader =
34 bincode::decode_from_slice(&from[..HEADER_SIZE], config)?.0;
35
36 Ok(header)
37 }
38}
39
40#[allow(clippy::upper_case_acronyms)]
41#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
42pub struct AndOTPItem {
43 pub secret: String,
44 #[zeroize(skip)]
45 pub issuer: String,
46 #[zeroize(skip)]
47 pub label: String,
48 #[zeroize(skip)]
49 pub digits: u32,
50 #[serde(rename = "type")]
51 #[zeroize(skip)]
52 pub method: Method,
53 #[zeroize(skip)]
54 pub algorithm: Algorithm,
55 #[zeroize(skip)]
56 pub thumbnail: Option<String>,
57 #[zeroize(skip)]
58 pub last_used: i64,
59 #[zeroize(skip)]
60 pub used_frequency: i32,
61 #[zeroize(skip)]
62 pub counter: Option<u32>,
63 #[zeroize(skip)]
64 pub tags: Vec<String>,
65 #[zeroize(skip)]
66 pub period: Option<u32>,
67}
68
69impl RestorableItem for AndOTPItem {
70 fn account(&self) -> String {
71 self.label.clone()
72 }
73
74 fn issuer(&self) -> String {
75 self.issuer.clone()
76 }
77
78 fn secret(&self) -> String {
79 self.secret.trim_end_matches('=').to_owned()
80 }
81
82 fn period(&self) -> Option<u32> {
83 self.period
84 }
85
86 fn method(&self) -> Method {
87 self.method
88 }
89
90 fn algorithm(&self) -> Algorithm {
91 self.algorithm
92 }
93
94 fn digits(&self) -> Option<u32> {
95 Some(self.digits)
96 }
97
98 fn counter(&self) -> Option<u32> {
99 self.counter
100 }
101}
102
103impl Backupable for AndOTP {
104 const ENCRYPTABLE: bool = false;
105 const IDENTIFIER: &'static str = "andotp";
106
107 fn title() -> String {
108 gettext("a_ndOTP")
110 }
111
112 fn subtitle() -> String {
113 gettext("Into a plain-text JSON file")
114 }
115
116 fn backup(model: &ProvidersModel, _key: Option<&str>) -> Result<Vec<u8>> {
117 let mut items = Vec::new();
118
119 for i in 0..model.n_items() {
120 let provider = model.item(i).and_downcast::<Provider>().unwrap();
121 let accounts = provider.accounts_model();
122
123 for j in 0..accounts.n_items() {
124 let account = accounts.item(j).and_downcast::<Account>().unwrap();
125
126 let otp_item = AndOTPItem {
127 secret: account.otp().secret(),
128 issuer: provider.name(),
129 label: account.name(),
130 digits: provider.digits(),
131 method: provider.method(),
132 algorithm: provider.algorithm(),
133 thumbnail: None,
134 last_used: 0,
135 used_frequency: 0,
136 counter: Some(account.counter()),
137 tags: vec![],
138 period: Some(provider.period()),
139 };
140 items.push(otp_item);
141 }
142 }
143
144 let content = serde_json::ser::to_string_pretty(&items)?;
145 Ok(content.as_bytes().to_vec())
146 }
147}
148
149impl Restorable for AndOTP {
150 const ENCRYPTABLE: bool = true;
151 const SCANNABLE: bool = false;
152 const IDENTIFIER: &'static str = "andotp";
153 type Item = AndOTPItem;
154
155 fn title() -> String {
156 gettext("an_dOTP")
158 }
159
160 fn subtitle() -> String {
161 gettext("From a plain-text JSON file")
162 }
163
164 fn restore_from_data(from: &[u8], key: Option<&str>) -> Result<Vec<Self::Item>> {
165 if let Some(key) = key {
166 AndOTP::decrypt(from, key.as_bytes())
167 } else {
168 let items: Vec<AndOTPItem> = serde_json::de::from_slice(from)?;
169 Ok(items)
170 }
171 }
172}
173
174impl AndOTP {
175 fn decrypt(from: &[u8], secret: &[u8]) -> Result<Vec<AndOTPItem>> {
176 let header = EncryptedAndOTPHeader::from_bytes(&from[..HEADER_SIZE])?;
177 let mut blob = from[HEADER_SIZE..].to_vec();
178
179 let iv = header.iv;
180 let iterations = std::num::NonZeroU32::new(header.iterations)
181 .context("AndOTP header has iterations set to 0")?;
182
183 let mut pbkdf2_key = [0; digest::SHA256_OUTPUT_LEN];
184 ring::pbkdf2::derive(
185 ring::pbkdf2::PBKDF2_HMAC_SHA1,
186 iterations,
187 &header.salt,
188 secret,
189 &mut pbkdf2_key,
190 );
191
192 let pbkdf2_key = UnboundKey::new(&AES_256_GCM, &pbkdf2_key)
193 .ok()
194 .context("Failed to generate unbound key")?;
195 let pbkdf2_key = LessSafeKey::new(pbkdf2_key);
196
197 let decrypted = pbkdf2_key
198 .open_in_place(Nonce::assume_unique_for_key(iv), Aad::empty(), &mut blob)
199 .ok()
200 .context("Error while decrypting")?;
201
202 let items: Vec<AndOTPItem> = serde_json::de::from_slice(decrypted)?;
203
204 Ok(items)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::{super::RestorableItem, *};
211 use crate::models::{Algorithm, Method};
212
213 #[test]
214 fn test_deserialize_header() {
215 let binary_header = [
216 0, 2, 69, 247, 55, 242, 73, 138, 187, 197, 27, 200, 251, 155, 241, 15, 178, 203, 129,
217 8, 36, 143, 1, 75, 219, 36, 241, 215,
218 ];
219
220 let header = EncryptedAndOTPHeader::from_bytes(&binary_header).unwrap();
221
222 assert_eq!(binary_header.len(), size_of::<EncryptedAndOTPHeader>());
223 assert_eq!(header.iterations, 148983);
224 assert_eq!(header.salt, binary_header[4..16]);
225 assert_eq!(header.iv, binary_header[16..]);
226 }
227
228 #[test]
229 fn test_andotp_decrypt() {
230 let data = std::fs::read("./src/backup/tests/andotp_enc.json.aes").unwrap();
232 let secret = b"123456";
233
234 let items = AndOTP::decrypt(&data, secret).unwrap();
235 assert_eq!(items.len(), 7);
236 }
237
238 #[test]
239 fn parse() {
240 let data = std::fs::read_to_string("./src/backup/tests/andotp_plain.json").unwrap();
241 let items = AndOTP::restore_from_data(data.as_bytes(), None).unwrap();
242
243 assert_eq!(items[0].account(), "Mason");
244 assert_eq!(items[0].issuer(), "Deno");
245 assert_eq!(items[0].secret(), "4SJHB4GSD43FZBAI7C2HLRJGPQ");
246 assert_eq!(items[0].period(), Some(30));
247 assert_eq!(items[0].method(), Method::TOTP);
248 assert_eq!(items[0].algorithm(), Algorithm::SHA1);
249 assert_eq!(items[0].digits(), Some(6));
250 assert_eq!(items[0].counter(), None);
251
252 assert_eq!(items[1].account(), "James");
253 assert_eq!(items[1].issuer(), "SPDX");
254 assert_eq!(items[1].secret(), "5OM4WOOGPLQEF6UGN3CPEOOLWU");
255 assert_eq!(items[1].period(), Some(20));
256 assert_eq!(items[1].method(), Method::TOTP);
257 assert_eq!(items[1].algorithm(), Algorithm::SHA256);
258 assert_eq!(items[1].digits(), Some(7));
259 assert_eq!(items[1].counter(), None);
260
261 assert_eq!(items[2].account(), "Elijah");
262 assert_eq!(items[2].issuer(), "Airbnb");
263 assert_eq!(items[2].secret(), "7ELGJSGXNCCTV3O6LKJWYFV2RA");
264 assert_eq!(items[2].period(), Some(50));
265 assert_eq!(items[2].method(), Method::TOTP);
266 assert_eq!(items[2].algorithm(), Algorithm::SHA512);
267 assert_eq!(items[2].digits(), Some(8));
268 assert_eq!(items[2].counter(), None);
269
270 assert_eq!(items[3].account(), "James");
271 assert_eq!(items[3].issuer(), "Issuu");
272 assert_eq!(items[3].secret(), "YOOMIXWS5GN6RTBPUFFWKTW5M4");
273 assert_eq!(items[3].period(), None);
274 assert_eq!(items[3].method(), Method::HOTP);
275 assert_eq!(items[3].algorithm(), Algorithm::SHA1);
276 assert_eq!(items[3].digits(), Some(6));
277 assert_eq!(items[3].counter(), Some(1));
278
279 assert_eq!(items[4].account(), "Benjamin");
280 assert_eq!(items[4].issuer(), "Air Canada");
281 assert_eq!(items[4].secret(), "KUVJJOM753IHTNDSZVCNKL7GII");
282 assert_eq!(items[4].period(), None);
283 assert_eq!(items[4].method(), Method::HOTP);
284 assert_eq!(items[4].algorithm(), Algorithm::SHA256);
285 assert_eq!(items[4].digits(), Some(7));
286 assert_eq!(items[4].counter(), Some(50));
287
288 assert_eq!(items[5].account(), "Mason");
289 assert_eq!(items[5].issuer(), "WWE");
290 assert_eq!(items[5].secret(), "5VAML3X35THCEBVRLV24CGBKOY");
291 assert_eq!(items[5].period(), None);
292 assert_eq!(items[5].method(), Method::HOTP);
293 assert_eq!(items[5].algorithm(), Algorithm::SHA512);
294 assert_eq!(items[5].digits(), Some(8));
295 assert_eq!(items[5].counter(), Some(10300));
296
297 assert_eq!(items[6].account(), "Sophia");
298 assert_eq!(items[6].issuer(), "Boeing");
299 assert_eq!(items[6].secret(), "JRZCL47CMXVOQMNPZR2F7J4RGI");
300 assert_eq!(items[6].period(), None);
301 assert_eq!(items[6].method(), Method::Steam);
302 assert_eq!(items[6].algorithm(), Algorithm::SHA1);
303 assert_eq!(items[6].digits(), Some(5));
304 assert_eq!(items[6].counter(), None);
305 }
306}