authenticator/backup/
google.rs
use anyhow::Result;
use gettextrs::gettext;
use percent_encoding::percent_decode;
use prost::{Enumeration, Message};
use url::Url;
use super::Restorable;
use crate::models::{Algorithm, Method, OTPUri};
pub struct Google;
impl Restorable for Google {
const ENCRYPTABLE: bool = false;
const SCANNABLE: bool = true;
const IDENTIFIER: &'static str = "google";
type Item = OTPUri;
fn title() -> String {
gettext("Google Authenticator")
}
fn subtitle() -> String {
gettext("From a QR code generated by Google Authenticator")
}
fn restore_from_data(from: &[u8], _key: Option<&str>) -> Result<Vec<Self::Item>> {
let string = String::from_utf8(from.into())?;
let uri = Url::parse(&string)?;
if uri.scheme() != "otpauth-migration" {
anyhow::bail!("Invalid OTP migration uri format, expected uri protocol to be otpauth-migration, got {}", uri.scheme());
}
if let Some(host) = uri.host_str() {
if host != "offline" {
anyhow::bail!(
"Invalid OTP migration uri format, expected uri host to be offline, got {host}"
);
}
} else {
anyhow::bail!(
"Invalid OTP migration uri format, expected uri host to be offline, got nothing"
);
}
let data = uri.query_pairs().fold(None, |folded, (key, value)| {
folded.or_else(|| match key.into_owned().as_str() {
"data" => {
let bytes = value.into_owned().into_bytes();
let decoded = percent_decode(&bytes);
let decoded = match data_encoding::BASE64.decode(&decoded.collect::<Vec<u8>>())
{
Ok(decoded) => decoded,
Err(_) => return None,
};
Some(match protobuf::MigrationPayload::decode(&*decoded) {
Ok(decoded) => decoded,
Err(_) => return None,
})
}
_ => None,
})
});
let data = if let Some(data) = data {
data
} else {
anyhow::bail!("Invalid OTP migration uri format, expected a data query parameter");
};
let data_len = data.otp_parameters.len();
let mut restored = data.otp_parameters.into_iter().fold(
Vec::with_capacity(data_len),
|mut folded, otp| {
folded.push(OTPUri {
algorithm: match otp.algorithm() {
protobuf::migration_payload::Algorithm::ALGO_INVALID => return folded,
protobuf::migration_payload::Algorithm::ALGO_SHA1 => Algorithm::SHA1,
},
digits: match otp.r#type() {
protobuf::migration_payload::OtpType::OTP_HOTP => Some(otp.digits as u32),
_ => None,
},
method: match otp.r#type() {
protobuf::migration_payload::OtpType::OTP_INVALID => return folded,
protobuf::migration_payload::OtpType::OTP_HOTP => Method::HOTP,
protobuf::migration_payload::OtpType::OTP_TOTP => Method::TOTP,
},
secret: {
let string = data_encoding::BASE32_NOPAD.encode(&otp.secret);
string.trim_end_matches(['\0', '=']).to_owned()
},
label: otp.name.clone(),
issuer: otp.issuer.clone(),
period: None,
counter: Some(otp.counter as u32),
});
folded
},
);
restored.shrink_to_fit();
Ok(restored)
}
}
#[allow(non_camel_case_types)]
mod protobuf {
use super::*;
#[derive(Message)]
pub struct MigrationPayload {
#[prost(message, repeated)]
pub otp_parameters: Vec<migration_payload::OtpParameters>,
#[prost(int32)]
pub version: i32,
#[prost(int32)]
pub batch_size: i32,
#[prost(int32)]
pub batch_index: i32,
#[prost(int32)]
pub batch_id: i32,
}
pub mod migration_payload {
use zeroize::{Zeroize, ZeroizeOnDrop};
use super::*;
#[derive(Debug, Enumeration)]
pub enum Algorithm {
ALGO_INVALID = 0,
ALGO_SHA1 = 1,
}
#[derive(Debug, Enumeration)]
pub enum OtpType {
OTP_INVALID = 0,
OTP_HOTP = 1,
OTP_TOTP = 2,
}
#[derive(Message, Zeroize, ZeroizeOnDrop)]
pub struct OtpParameters {
#[prost(bytes)]
pub secret: Vec<u8>,
#[zeroize(skip)]
#[prost(string)]
pub name: String,
#[prost(string)]
#[zeroize(skip)]
pub issuer: String,
#[prost(enumeration = "Algorithm")]
#[zeroize(skip)]
pub algorithm: i32,
#[prost(int32)]
#[zeroize(skip)]
pub digits: i32,
#[prost(enumeration = "OtpType")]
#[zeroize(skip)]
pub r#type: i32,
#[prost(int64)]
#[zeroize(skip)]
pub counter: i64,
}
}
}
#[cfg(test)]
mod tests {
use super::{super::RestorableItem, *};
#[test]
fn parse() {
let data = b"otpauth-migration://offline?data=CjYKEExyJfPiZeroMa/MdF%2BnkTISE2pvaG5kb2VAZXhhbXBsZS5jb20aB0Rpc2NvcmQgASgBMAIQARgBIAA%3D";
let items = Google::restore_from_data(data, None).unwrap();
assert_eq!(items[0].account(), "johndoe@example.com");
assert_eq!(items[0].issuer(), "Discord");
assert_eq!(items[0].secret(), "JRZCL47CMXVOQMNPZR2F7J4RGI");
assert_eq!(items[0].period(), None);
assert_eq!(items[0].algorithm(), Algorithm::SHA1);
assert_eq!(items[0].digits(), None);
assert_eq!(items[0].counter(), Some(0));
}
}