Skip to main content

authenticator/backup/
google.rs

1use anyhow::Result;
2use gettextrs::gettext;
3use percent_encoding::percent_decode;
4use prost::{Enumeration, Message};
5use url::Url;
6
7use super::Restorable;
8use crate::models::{Algorithm, Method, OTPUri};
9
10pub struct Google;
11
12impl Restorable for Google {
13    const ENCRYPTABLE: bool = false;
14    const SCANNABLE: bool = true;
15    const IDENTIFIER: &'static str = "google";
16    type Item = OTPUri;
17
18    fn title() -> String {
19        gettext("Google Authenticator")
20    }
21
22    fn subtitle() -> String {
23        gettext("From a QR code generated by Google Authenticator")
24    }
25
26    fn restore_from_data(from: &[u8], _key: Option<&str>) -> Result<Vec<Self::Item>> {
27        let string = String::from_utf8(from.into())?;
28        let uri = Url::parse(&string)?;
29
30        if uri.scheme() != "otpauth-migration" {
31            anyhow::bail!(
32                "Invalid OTP migration uri format, expected uri protocol to be otpauth-migration, got {}",
33                uri.scheme()
34            );
35        }
36
37        if let Some(host) = uri.host_str() {
38            if host != "offline" {
39                anyhow::bail!(
40                    "Invalid OTP migration uri format, expected uri host to be offline, got {host}"
41                );
42            }
43        } else {
44            anyhow::bail!(
45                "Invalid OTP migration uri format, expected uri host to be offline, got nothing"
46            );
47        }
48
49        let data = uri.query_pairs().fold(None, |folded, (key, value)| {
50            folded.or_else(|| match key.into_owned().as_str() {
51                "data" => {
52                    let bytes = value.into_owned().into_bytes();
53                    let decoded = percent_decode(&bytes);
54                    let decoded = match data_encoding::BASE64.decode(&decoded.collect::<Vec<u8>>())
55                    {
56                        Ok(decoded) => decoded,
57                        Err(_) => return None,
58                    };
59                    Some(match protobuf::MigrationPayload::decode(&*decoded) {
60                        Ok(decoded) => decoded,
61                        Err(_) => return None,
62                    })
63                }
64                _ => None,
65            })
66        });
67
68        let data = if let Some(data) = data {
69            data
70        } else {
71            anyhow::bail!("Invalid OTP migration uri format, expected a data query parameter");
72        };
73
74        let data_len = data.otp_parameters.len();
75
76        let mut restored = data.otp_parameters.into_iter().fold(
77            Vec::with_capacity(data_len),
78            |mut folded, otp| {
79                folded.push(OTPUri {
80                    algorithm: match otp.algorithm() {
81                        protobuf::migration_payload::Algorithm::ALGO_INVALID => return folded,
82                        protobuf::migration_payload::Algorithm::ALGO_SHA1 => Algorithm::SHA1,
83                    },
84                    digits: match otp.r#type() {
85                        protobuf::migration_payload::OtpType::OTP_HOTP => Some(otp.digits as u32),
86                        _ => None,
87                    },
88                    method: match otp.r#type() {
89                        protobuf::migration_payload::OtpType::OTP_INVALID => return folded,
90                        protobuf::migration_payload::OtpType::OTP_HOTP => Method::HOTP,
91                        protobuf::migration_payload::OtpType::OTP_TOTP => Method::TOTP,
92                    },
93                    secret: {
94                        let string = data_encoding::BASE32_NOPAD.encode(&otp.secret);
95
96                        string.trim_end_matches(['\0', '=']).to_owned()
97                    },
98                    label: otp.name.clone(),
99                    issuer: otp.issuer.clone(),
100                    period: None,
101                    counter: Some(otp.counter as u32),
102                });
103                folded
104            },
105        );
106
107        restored.shrink_to_fit();
108
109        Ok(restored)
110    }
111}
112
113#[allow(non_camel_case_types)]
114mod protobuf {
115    use super::*;
116
117    #[derive(Message)]
118    pub struct MigrationPayload {
119        #[prost(message, repeated)]
120        pub otp_parameters: Vec<migration_payload::OtpParameters>,
121        #[prost(int32)]
122        pub version: i32,
123        #[prost(int32)]
124        pub batch_size: i32,
125        #[prost(int32)]
126        pub batch_index: i32,
127        #[prost(int32)]
128        pub batch_id: i32,
129    }
130
131    pub mod migration_payload {
132        use zeroize::{Zeroize, ZeroizeOnDrop};
133
134        use super::*;
135
136        #[derive(Debug, Enumeration)]
137        pub enum Algorithm {
138            ALGO_INVALID = 0,
139            ALGO_SHA1 = 1,
140        }
141
142        #[derive(Debug, Enumeration)]
143        pub enum OtpType {
144            OTP_INVALID = 0,
145            OTP_HOTP = 1,
146            OTP_TOTP = 2,
147        }
148
149        #[derive(Message, Zeroize, ZeroizeOnDrop)]
150        pub struct OtpParameters {
151            #[prost(bytes)]
152            pub secret: Vec<u8>,
153            #[zeroize(skip)]
154            #[prost(string)]
155            pub name: String,
156            #[prost(string)]
157            #[zeroize(skip)]
158            pub issuer: String,
159            #[prost(enumeration = "Algorithm")]
160            #[zeroize(skip)]
161            pub algorithm: i32,
162            #[prost(int32)]
163            #[zeroize(skip)]
164            pub digits: i32,
165            #[prost(enumeration = "OtpType")]
166            #[zeroize(skip)]
167            pub r#type: i32,
168            #[prost(int64)]
169            #[zeroize(skip)]
170            pub counter: i64,
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{super::RestorableItem, *};
178
179    #[test]
180    fn parse() {
181        let data = b"otpauth-migration://offline?data=CjYKEExyJfPiZeroMa/MdF%2BnkTISE2pvaG5kb2VAZXhhbXBsZS5jb20aB0Rpc2NvcmQgASgBMAIQARgBIAA%3D";
182        let items = Google::restore_from_data(data, None).unwrap();
183
184        assert_eq!(items[0].account(), "johndoe@example.com");
185        assert_eq!(items[0].issuer(), "Discord");
186        assert_eq!(items[0].secret(), "JRZCL47CMXVOQMNPZR2F7J4RGI");
187        assert_eq!(items[0].period(), None);
188        assert_eq!(items[0].algorithm(), Algorithm::SHA1);
189        assert_eq!(items[0].digits(), None);
190        assert_eq!(items[0].counter(), Some(0));
191    }
192}