authenticator/backup/
raivootp.rs

1use 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/// A Raivo OTP entry.
17///
18/// [See Raivo's source code for each item's serialized form.][0]
19///
20/// [0]: https://github.com/raivo-otp/ios-application/blob/3a8aaa0ea16a761e6205abd2700ac90dd4c9c9b6/Raivo/Models/Password.swift#L104-L116
21#[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    /// Restore from a ZIP file generated by Raivo OTP.
110    ///
111    /// See Raivo's source code for [exporting the AES-encrypted ZIP][0].
112    ///
113    /// [0]: https://github.com/raivo-otp/ios-application/blob/3a8aaa0ea16a761e6205abd2700ac90dd4c9c9b6/Raivo/Features/DataExportFeature.swift#L188-L195
114    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}