authenticator/backup/
yandex.rs

1use std::str::FromStr;
2
3use anyhow::Result;
4use gettextrs::gettext;
5use serde::Deserialize;
6
7use super::Restorable;
8use crate::models::{Method, OTPUri};
9
10pub struct Yandex;
11
12impl Restorable for Yandex {
13    const ENCRYPTABLE: bool = false;
14    const SCANNABLE: bool = false;
15    const IDENTIFIER: &'static str = "yandex-txt";
16    type Item = OTPUri;
17
18    fn title() -> String {
19        gettext("Yandex ID")
20    }
21
22    fn subtitle() -> String {
23        gettext("From a plain-text file generated by Yandex ID")
24    }
25
26    fn restore_from_data(from: &[u8], _key: Option<&str>) -> Result<Vec<Self::Item>> {
27        #[derive(Deserialize)]
28        struct Entry {
29            #[serde(rename = "techInfo")]
30            tech_info: String,
31            #[serde(rename = "warning")]
32            _warning: Option<String>,
33        }
34
35        let raw: Vec<Entry> = serde_json::from_slice(from)?;
36        let mut items = Vec::with_capacity(raw.len());
37
38        for entry in raw {
39            let Ok(mut normalized) = OTPUri::from_str(&entry.tech_info) else {
40                continue;
41            };
42
43            // Working with yaotp is not currently supported
44            if !matches!(normalized.method, Method::TOTP | Method::HOTP) {
45                continue;
46            }
47
48            match normalized.method {
49                Method::TOTP => {
50                    normalized.digits = None;
51                    normalized.counter = None;
52                }
53                Method::HOTP => {
54                    normalized.counter.get_or_insert(0);
55                }
56                Method::Steam => {
57                    continue;
58                }
59            }
60
61            items.push(normalized);
62        }
63
64        Ok(items)
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::{
72        backup::RestorableItem,
73        models::{Algorithm, Method},
74    };
75
76    #[test]
77    fn parse_yandex_json_anonymous() {
78        let data = r#"[
79            {
80                "techInfo": "otpauth://yaotp/user?chars=alpha&secret=REDACTEDSECRET&creation_type=unknown"
81            },
82            {
83                "name": "user1@example.com",
84                "secret": "ABCDEFGHIJKLMNOP",
85                "techInfo": "otpauth://totp/Issuer1:user1@example.com?algorithm=SHA256&digits=8&secret=ABCDEFGHIJKLMNOP&issuer=Issuer1&period=45"
86            },
87            {
88                "name": "user2@example.com",
89                "secret": "QRSTUVWXYZ123456",
90                "techInfo": "otpauth://hotp/Issuer2:user2@example.com?algorithm=SHA512&digits=7&secret=QRSTUVWXYZ123456&issuer=Issuer2&counter=5"
91            }
92        ]"#;
93
94        // Working with yaotp is not currently supported
95        let items = Yandex::restore_from_data(data.as_bytes(), None).unwrap();
96        assert_eq!(items.len(), 2);
97
98        let totp = items.iter().find(|i| i.method() == Method::TOTP).unwrap();
99        assert_eq!(totp.issuer(), "Issuer1");
100        assert_eq!(totp.account(), "user1@example.com");
101        assert_eq!(totp.secret(), "ABCDEFGHIJKLMNOP");
102        assert_eq!(totp.method(), Method::TOTP);
103        assert_eq!(totp.algorithm(), Algorithm::SHA256);
104        assert_eq!(totp.digits(), None);
105        assert_eq!(totp.period(), Some(45));
106
107        let hotp = items.iter().find(|i| i.method() == Method::HOTP).unwrap();
108        assert_eq!(hotp.issuer(), "Issuer2");
109        assert_eq!(hotp.account(), "user2@example.com");
110        assert_eq!(hotp.secret(), "QRSTUVWXYZ123456");
111        assert_eq!(hotp.method(), Method::HOTP);
112        assert_eq!(hotp.algorithm(), Algorithm::SHA512);
113        assert_eq!(hotp.digits(), Some(7));
114        assert_eq!(hotp.counter(), Some(5));
115    }
116}