use data_encoding::BASE32_NOPAD;
use serde::{Deserialize, Serialize};
use crate::otp::{otp_algorithm::OTPAlgorithm, otp_element::OTPElement, otp_type::OTPType};
#[derive(Serialize, Deserialize)]
pub struct FreeOTPPlusJson {
#[serde(rename = "tokenOrder")]
token_order: Vec<String>,
tokens: Vec<FreeOTPElement>,
}
impl FreeOTPPlusJson {
pub fn new(tokens: Vec<FreeOTPElement>) -> Self {
let token_order: Vec<String> = tokens
.iter()
.map(|e| {
if e.issuer_ext.is_empty() {
e.label.clone()
} else {
format!("{}:{}", e.issuer_ext, e.label)
}
})
.collect();
Self {
token_order,
tokens,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct FreeOTPElement {
pub algo: String,
pub counter: u64,
pub digits: u64,
#[serde(rename = "issuerExt")]
pub issuer_ext: String,
pub label: String,
pub period: u64,
pub secret: Vec<i8>,
pub r#type: String,
}
impl From<FreeOTPElement> for OTPElement {
fn from(token: FreeOTPElement) -> Self {
let counter: Option<u64> = if token.algo.to_uppercase().as_str() == "HOTP" {
Some(token.counter)
} else {
None
};
OTPElement {
counter,
secret: encode_secret(&token.secret),
issuer: token.issuer_ext,
label: token.label,
digits: token.digits,
type_: OTPType::from(token.r#type.as_str()),
algorithm: OTPAlgorithm::from(token.algo.as_str()),
period: token.period,
pin: None,
}
}
}
impl TryFrom<FreeOTPPlusJson> for Vec<OTPElement> {
type Error = String;
fn try_from(freeotp: FreeOTPPlusJson) -> Result<Self, Self::Error> {
Ok(freeotp.tokens.into_iter().map(Into::into).collect())
}
}
fn encode_secret(secret: &[i8]) -> String {
BASE32_NOPAD.encode(
secret
.iter()
.map(|n| *n as u8)
.collect::<Vec<u8>>()
.as_slice(),
)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{
importers::{freeotp_plus::FreeOTPElement, importer::import_from_path},
otp::{otp_algorithm::OTPAlgorithm, otp_element::OTPElement, otp_type::OTPType},
};
use std::fs;
use crate::otp::otp_element::OTPDatabase;
use color_eyre::Result;
use super::{FreeOTPPlusJson, encode_secret};
#[test]
fn test_secret_conversion() {
let secret: Vec<i8> = vec![
-34, 104, -37, -33, 82, -89, -93, -105, -14, 5, 100, -73, -84, 1, 11, 73, 101, -92,
106, 122, -90, 111, -119, 30, 87, -6, 16, -57, -126, 25, 0, -65, -35, -76, -38,
];
assert_eq!(
encode_secret(&secret),
String::from("3ZUNXX2SU6RZP4QFMS32YAILJFS2I2T2UZXYSHSX7IIMPAQZAC753NG2")
);
}
#[test]
fn test_conversion() {
let imported = import_from_path::<FreeOTPPlusJson>(PathBuf::from(
"test_samples/freeotp_plus_example1.json",
));
assert_eq!(
vec![
OTPElement {
secret: "AAAAAAAAAAAAAAAA".to_string(),
issuer: "Example2".to_string(),
label: "Label2".to_string(),
digits: 6,
type_: OTPType::Totp,
algorithm: OTPAlgorithm::Sha1,
period: 30,
counter: None,
pin: None
},
OTPElement {
secret: "AAAAAAAA".to_string(),
issuer: "Example1".to_string(),
label: "Label1".to_string(),
digits: 6,
type_: OTPType::Totp,
algorithm: OTPAlgorithm::Sha256,
period: 30,
counter: None,
pin: None
}
],
imported.unwrap()
);
}
#[test]
fn test_freeotp_export() {
let input_json: String = fs::read_to_string(PathBuf::from("test_samples/cotp_input.json"))
.expect("Cannot read input file for test");
let input_cotp_database: OTPDatabase =
serde_json::from_str(input_json.as_str()).expect("Cannot deserialize into input JSON");
let converted: Result<FreeOTPPlusJson> = (&input_cotp_database).try_into();
let free_otp = converted.unwrap();
assert_eq!(
vec!["label1".to_string(), "ciccio:label2".to_string()],
free_otp.token_order
);
assert_eq!(
vec![
FreeOTPElement {
algo: "SHA1".to_string(),
counter: 0,
digits: 6,
issuer_ext: String::default(),
label: "label1".to_string(),
period: 30,
secret: vec![
7, -40, 73, 126, -112, -25, 37, 28, 72, -39, 115, 50, -127, 46, 74, 117,
-40, 124, -109, 58, -19, 54, 35, 117, -120, -106, -40, -39, -116, 107,
-123, 127, 111, -93, -71, 6, 92, -116, 31, 4, 103, -59, 75, -106, 57, 54,
-3, 104, 103, -26, -57, 59, -69, 98, -16, -102, 91, 89, 98, 90, -100, -21,
44, 28, -105, -45, 92, -128, 82, 30, -23, -105, -30, 91, 17, -51, 24, -7,
-61, 75, -38, -116, -122, 106, 79, 37, 82, -62, -125, -30, -27, 116, 116,
82, -55, 72, 87, 41, 15, -25, -27, 65, 6, -104, 49, -26, -111, 10
],
r#type: "TOTP".to_string()
},
FreeOTPElement {
algo: "SHA256".to_string(),
counter: 3,
digits: 6,
issuer_ext: "ciccio".to_string(),
label: "label2".to_string(),
period: 30,
secret: vec![
35, -75, 13, 47, -2, -128, -100, -27, 64, -115, -72, 14, -78, -122, 88, 62,
-32, 57, 37, -111, 90, -70, -58, -15, -113, 111, -94, 91, -90, 90, -91, 61,
-9, -23, 54, 4, -31, -93, -8, -9, 27, 125, -21, 112, -80, -30, 64, 46, 10
],
r#type: "HOTP".to_string()
}
],
free_otp.tokens
);
}
}