authenticator/backup/
google.rs1use 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}