authenticator/backup/
raivootp.rs1use std::io::Cursor;
2
3use anyhow::Result;
4use gettextrs::gettext;
5use serde::{Deserialize, Serialize, de::Deserializer};
6use zeroize::{Zeroize, ZeroizeOnDrop};
7use zip::{self, ZipArchive};
8
9use super::{Restorable, RestorableItem};
10use crate::models::{Algorithm, Method};
11
12#[allow(clippy::upper_case_acronyms)]
13#[derive(Serialize, Deserialize)]
14pub struct RaivoOTP;
15
16#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
22pub struct Item {
23 #[zeroize(skip)]
24 issuer: String,
25 #[zeroize(skip)]
26 account: String,
27 secret: String,
28 #[zeroize(skip)]
29 algorithm: Algorithm,
30 #[serde(deserialize_with = "deserialize_raivo_u32")]
31 #[zeroize(skip)]
32 digits: Option<u32>,
33 #[serde(rename = "kind")]
34 #[zeroize(skip)]
35 method: Method,
36 #[serde(rename = "timer")]
37 #[serde(deserialize_with = "deserialize_raivo_u32")]
38 #[zeroize(skip)]
39 period: Option<u32>,
40 #[serde(deserialize_with = "deserialize_raivo_u32")]
41 #[zeroize(skip)]
42 counter: Option<u32>,
43}
44
45fn deserialize_raivo_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
46where
47 D: Deserializer<'de>,
48{
49 let n: u32 = String::deserialize(deserializer)?
50 .parse()
51 .map_err(serde::de::Error::custom)?;
52 Ok(Some(n))
53}
54
55impl RestorableItem for Item {
56 fn account(&self) -> String {
57 self.account.clone()
58 }
59
60 fn issuer(&self) -> String {
61 self.issuer.clone()
62 }
63
64 fn secret(&self) -> String {
65 self.secret.clone()
66 }
67
68 fn period(&self) -> Option<u32> {
69 match self.method() {
70 Method::TOTP => self.period,
71 Method::HOTP | Method::Steam => None,
72 }
73 }
74
75 fn method(&self) -> Method {
76 self.method
77 }
78
79 fn algorithm(&self) -> Algorithm {
80 self.algorithm
81 }
82
83 fn digits(&self) -> Option<u32> {
84 self.digits
85 }
86
87 fn counter(&self) -> Option<u32> {
88 match self.method() {
89 Method::HOTP => self.counter,
90 Method::TOTP | Method::Steam => None,
91 }
92 }
93}
94
95impl Restorable for RaivoOTP {
96 const ENCRYPTABLE: bool = true;
97 const SCANNABLE: bool = false;
98 const IDENTIFIER: &'static str = "raivootp";
99 type Item = Item;
100
101 fn title() -> String {
102 gettext("Raivo OTP")
103 }
104
105 fn subtitle() -> String {
106 gettext("From a ZIP export generated by Raivo OTP")
107 }
108
109 fn restore_from_data(from: &[u8], key: Option<&str>) -> Result<Vec<Self::Item>> {
115 let password: &[u8] = match key {
116 None => &[],
117 Some(k) => k.as_bytes(),
118 };
119 let mut archive = ZipArchive::new(Cursor::new(from))?;
120 let file = archive.by_name_decrypt("raivo-otp-export.json", password)?;
121 let items = serde_json::from_reader(file)?;
122 Ok(items)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::{super::RestorableItem, *};
129 use crate::models::{Algorithm, Method};
130
131 #[test]
132 fn parse() {
133 let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
134 let items = RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).unwrap();
135
136 assert_eq!(items.len(), 2);
137
138 assert_eq!(items[0].account(), "mason");
139 assert_eq!(items[0].issuer(), "Example A");
140 assert_eq!(items[0].secret(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789");
141 assert_eq!(items[0].period(), Some(30));
142 assert_eq!(items[0].method(), Method::TOTP);
143 assert_eq!(items[0].algorithm(), Algorithm::SHA1);
144 assert_eq!(items[0].digits(), Some(6));
145 assert_eq!(items[0].counter(), None);
146
147 assert_eq!(items[1].account(), "james");
148 assert_eq!(items[1].issuer(), "Example B");
149 assert_eq!(items[1].secret(), "12345678ABCDEFGHIJKLMNOPQRSTUVWXYZ");
150 assert_eq!(items[1].period(), Some(123));
151 assert_eq!(items[1].method(), Method::TOTP);
152 assert_eq!(items[1].algorithm(), Algorithm::SHA256);
153 assert_eq!(items[1].digits(), Some(8));
154 assert_eq!(items[1].counter(), None);
155 }
156
157 #[test]
158 fn invalid_zip() {
159 let data: [u8; 3] = [1, 2, 3];
160 assert!(RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).is_err());
161 }
162
163 #[test]
164 fn invalid_password() {
165 let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
166 assert!(RaivoOTP::restore_from_data(&data, Some("bad password")).is_err());
167 }
168}