authenticator/backup/
raivootp.rs
use std::io::Cursor;
use anyhow::Result;
use gettextrs::gettext;
use serde::{de::Deserializer, Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
use zip::{self, ZipArchive};
use super::{Restorable, RestorableItem};
use crate::models::{Algorithm, Method};
#[allow(clippy::upper_case_acronyms)]
#[derive(Serialize, Deserialize)]
pub struct RaivoOTP;
#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct Item {
#[zeroize(skip)]
issuer: String,
#[zeroize(skip)]
account: String,
secret: String,
#[zeroize(skip)]
algorithm: Algorithm,
#[serde(deserialize_with = "deserialize_raivo_u32")]
#[zeroize(skip)]
digits: Option<u32>,
#[serde(rename = "kind")]
#[zeroize(skip)]
method: Method,
#[serde(rename = "timer")]
#[serde(deserialize_with = "deserialize_raivo_u32")]
#[zeroize(skip)]
period: Option<u32>,
#[serde(deserialize_with = "deserialize_raivo_u32")]
#[zeroize(skip)]
counter: Option<u32>,
}
fn deserialize_raivo_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
D: Deserializer<'de>,
{
let n: u32 = String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)?;
Ok(Some(n))
}
impl RestorableItem for Item {
fn account(&self) -> String {
self.account.clone()
}
fn issuer(&self) -> String {
self.issuer.clone()
}
fn secret(&self) -> String {
self.secret.clone()
}
fn period(&self) -> Option<u32> {
match self.method() {
Method::TOTP => self.period,
Method::HOTP | Method::Steam => None,
}
}
fn method(&self) -> Method {
self.method
}
fn algorithm(&self) -> Algorithm {
self.algorithm
}
fn digits(&self) -> Option<u32> {
self.digits
}
fn counter(&self) -> Option<u32> {
match self.method() {
Method::HOTP => self.counter,
Method::TOTP | Method::Steam => None,
}
}
}
impl Restorable for RaivoOTP {
const ENCRYPTABLE: bool = true;
const SCANNABLE: bool = false;
const IDENTIFIER: &'static str = "raivootp";
type Item = Item;
fn title() -> String {
gettext("Raivo OTP")
}
fn subtitle() -> String {
gettext("From a ZIP export generated by Raivo OTP")
}
fn restore_from_data(from: &[u8], key: Option<&str>) -> Result<Vec<Self::Item>> {
let password: &[u8] = match key {
None => &[],
Some(k) => k.as_bytes(),
};
let mut archive = ZipArchive::new(Cursor::new(from))?;
let file = archive.by_name_decrypt("raivo-otp-export.json", password)?;
let items = serde_json::from_reader(file)?;
Ok(items)
}
}
#[cfg(test)]
mod tests {
use super::{super::RestorableItem, *};
use crate::models::{Algorithm, Method};
#[test]
fn parse() {
let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
let items = RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].account(), "mason");
assert_eq!(items[0].issuer(), "Example A");
assert_eq!(items[0].secret(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789");
assert_eq!(items[0].period(), Some(30));
assert_eq!(items[0].method(), Method::TOTP);
assert_eq!(items[0].algorithm(), Algorithm::SHA1);
assert_eq!(items[0].digits(), Some(6));
assert_eq!(items[0].counter(), None);
assert_eq!(items[1].account(), "james");
assert_eq!(items[1].issuer(), "Example B");
assert_eq!(items[1].secret(), "12345678ABCDEFGHIJKLMNOPQRSTUVWXYZ");
assert_eq!(items[1].period(), Some(123));
assert_eq!(items[1].method(), Method::TOTP);
assert_eq!(items[1].algorithm(), Algorithm::SHA256);
assert_eq!(items[1].digits(), Some(8));
assert_eq!(items[1].counter(), None);
}
#[test]
fn invalid_zip() {
let data: [u8; 3] = [1, 2, 3];
assert!(RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).is_err());
}
#[test]
fn invalid_password() {
let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
assert!(RaivoOTP::restore_from_data(&data, Some("bad password")).is_err());
}
}