Merge pull request #94 from dyc3/secrets

Implement a special type to hold 2fa secret that gets zeroed on drop
This commit is contained in:
Carson McManus 2021-08-25 00:23:28 -04:00 committed by GitHub
commit 140b2abda6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 215 additions and 74 deletions

15
Cargo.lock generated
View file

@ -1552,6 +1552,16 @@ dependencies = [
"tendril", "tendril",
] ]
[[package]]
name = "secrecy"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
dependencies = [
"serde",
"zeroize",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.1.2" version = "2.1.2"
@ -1830,6 +1840,7 @@ dependencies = [
"reqwest", "reqwest",
"rsa", "rsa",
"scraper", "scraper",
"secrecy",
"serde", "serde",
"serde_json", "serde_json",
"standback", "standback",
@ -2430,9 +2441,9 @@ dependencies = [
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.2.0" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd"
dependencies = [ dependencies = [
"zeroize_derive", "zeroize_derive",
] ]

View file

@ -243,21 +243,24 @@ mod tests {
} }
#[test] #[test]
fn test_should_save_and_load_manifest() { fn test_should_save_and_load_manifest() -> anyhow::Result<()> {
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let tmp_dir = TempDir::new("steamguard-cli-test")?;
let manifest_path = tmp_dir.path().join("manifest.json"); let manifest_path = tmp_dir.path().join("manifest.json");
println!("tempdir: {}", manifest_path.display());
let mut manifest = Manifest::new(manifest_path.as_path()); let mut manifest = Manifest::new(manifest_path.as_path());
let mut account = SteamGuardAccount::new(); let mut account = SteamGuardAccount::new();
account.account_name = "asdf1234".into(); account.account_name = "asdf1234".into();
account.revocation_code = "R12345".into(); account.revocation_code = "R12345".into();
account.shared_secret = "secret".into(); account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
)?;
manifest.add_account(account); manifest.add_account(account);
assert!(matches!(manifest.save(&None), Ok(_))); manifest.save(&None)?;
let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); let mut loaded_manifest = Manifest::load(manifest_path.as_path())?;
assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries.len(), 1);
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
assert!(matches!(loaded_manifest.load_accounts(&None), Ok(_))); loaded_manifest.load_accounts(&None)?;
assert_eq!( assert_eq!(
loaded_manifest.entries.len(), loaded_manifest.entries.len(),
loaded_manifest.accounts.len() loaded_manifest.accounts.len()
@ -272,8 +275,11 @@ mod tests {
); );
assert_eq!( assert_eq!(
loaded_manifest.accounts[0].lock().unwrap().shared_secret, loaded_manifest.accounts[0].lock().unwrap().shared_secret,
"secret" steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
)?,
); );
return Ok(());
} }
#[test] #[test]
@ -285,7 +291,10 @@ mod tests {
let mut account = SteamGuardAccount::new(); let mut account = SteamGuardAccount::new();
account.account_name = "asdf1234".into(); account.account_name = "asdf1234".into();
account.revocation_code = "R12345".into(); account.revocation_code = "R12345".into();
account.shared_secret = "secret".into(); account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
)
.unwrap();
manifest.add_account(account); manifest.add_account(account);
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
assert!(matches!(manifest.save(&passkey), Ok(_))); assert!(matches!(manifest.save(&passkey), Ok(_)));
@ -308,7 +317,10 @@ mod tests {
); );
assert_eq!( assert_eq!(
loaded_manifest.accounts[0].lock().unwrap().shared_secret, loaded_manifest.accounts[0].lock().unwrap().shared_secret,
"secret" steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
)
.unwrap(),
); );
} }
@ -321,7 +333,10 @@ mod tests {
let mut account = SteamGuardAccount::new(); let mut account = SteamGuardAccount::new();
account.account_name = "asdf1234".into(); account.account_name = "asdf1234".into();
account.revocation_code = "R12345".into(); account.revocation_code = "R12345".into();
account.shared_secret = "secret".into(); account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
)
.unwrap();
account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into(); account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into();
account.token_gid = "asdf1234".into(); account.token_gid = "asdf1234".into();
manifest.add_account(account); manifest.add_account(account);
@ -346,7 +361,10 @@ mod tests {
); );
assert_eq!( assert_eq!(
loaded_manifest.accounts[0].lock().unwrap().shared_secret, loaded_manifest.accounts[0].lock().unwrap().shared_secret,
"secret" steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
)
.unwrap(),
); );
return Ok(()); return Ok(());
@ -360,7 +378,10 @@ mod tests {
let mut account = SteamGuardAccount::new(); let mut account = SteamGuardAccount::new();
account.account_name = "asdf1234".into(); account.account_name = "asdf1234".into();
account.revocation_code = "R12345".into(); account.revocation_code = "R12345".into();
account.shared_secret = "secret".into(); account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
)
.unwrap();
manifest.add_account(account); manifest.add_account(account);
assert!(matches!(manifest.save(&None), Ok(_))); assert!(matches!(manifest.save(&None), Ok(_)));
std::fs::remove_file(&manifest_path).unwrap(); std::fs::remove_file(&manifest_path).unwrap();
@ -391,7 +412,10 @@ mod tests {
); );
assert_eq!( assert_eq!(
loaded_manifest.accounts[0].lock().unwrap().shared_secret, loaded_manifest.accounts[0].lock().unwrap().shared_secret,
"secret" steamguard::token::TwoFactorSecret::parse_shared_secret(
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
)
.unwrap(),
); );
} }

View file

@ -100,12 +100,13 @@ impl EntryEncryptor for LegacySdaCompatible {
let origsize = plaintext.len(); let origsize = plaintext.len();
let buffersize: usize = (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16; let buffersize: usize = (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16;
let mut buffer = vec![]; let mut buffer = vec![];
for chunk in plaintext.as_slice().chunks(256) { for chunk in plaintext.as_slice().chunks(128) {
let chunksize = chunk.len(); let chunksize = chunk.len();
let buffersize = (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16; let buffersize = (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16;
let mut chunkbuffer = vec![0xffu8; buffersize]; let mut chunkbuffer = vec![0xffu8; buffersize];
chunkbuffer[..chunksize].copy_from_slice(&chunk); chunkbuffer[..chunksize].copy_from_slice(&chunk);
if buffersize != chunksize { if buffersize != chunksize {
// pad the last chunk
chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize) chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize)
.unwrap() .unwrap()
.to_vec(); .to_vec();

View file

@ -1 +1 @@
ruZBuGG4+r5RU4JPjjHgkBLB205+1qSJjikDR5jrOpZsYgPSdJV9N4Rp7OnOQ63YBSiu1JpoNIUrSxJnW4UaPBIkgbWHsAngSiuZAhkZq/FJuRie0OcGN2NnwuE2xZUQNfREqU4DTiq8VLRBqvHTueeI5pReJ1vGradhjOgyWVG4MxMlVylVTIPqmZf1NrfBQbEL6Nip3dRGdzXsLhtnxhr8/0meBNOwtk+5sm6b+XVEd81aSsQNYEK5RUdmzNbOdK+UGUTOgACw6rBoBUP3zpv7U0gEC7u+iIIv1CQXp0HUH4p06Edu8r0APDQHIgt8/WmnRtm6EAWfgo/RxbMoTFTKo6Qfa18baGCkQnNJPiiIIQc1e4/31mw9DbGhDfkYJL4O5A9wbWeSRg92qxe2d4odHx0NIfyZ9CsKfIYc4/azq0I7K3hjcpt5JgYOQowm4YrYMQrmkyw90HMAMcOHoKtMVU5i58JumY3cYAnf+skCOra29D0Py7k0mGqm+9W2OWmO+XE3QfhuOK0FwFts5umyVI5AK4qZG90ioPt6CHgDZKgCdzuV7iTWwhuhd+EYalcbuSAQHX5cnNVVt9Z+0c9t+fnjz0t9w8wChpKhRcFGUeMvzOFO0lhP5kneJ3N/xKo0bhO2spjzzUw8lXE5wFv7TFmr058m+rzW/ucvtcl6KSvvr0RGNBcwUo+G8Q4jvJNTDFhqT0ElnWpgF2Rxb9m5UKZz5bfLszlf0KbSO2ZXg9wO65/itqXQnKzq1ALI ZQqJk4KW7pb5bndkra244z3ttIks58UplODn5IgljrOK3bKORSwrnZYb9Iv6YsirmrT0hQ3tx381GhQu4Nyj/PFHHCfFTrduaCoMLRrHDsmsexOW93Yo02acHXNfPSvxtjGfZpsuIZVlhVy8JDH/ESXp88cKn2zqjXQWu6pLnah1YPlpZuTfArw69+Em7V1OH1CoKnsYuhCo/x4u7fXhMNgDlRBvDbO4enGzaixonPu9er5Zp6iNEeuAUqmD0DHASygNmzBhUBHv8Avng8YKbvu611yQVT2KybnIoL+Q11Y36GoFhWskmG3lLwh/1OlReGwJ1iX0lDDthoel/Ygj6EC2+wkR8V7eMQf48R15xdBILYTzcrjtiuLCr9MBc/HM/ToEa3QCGwGkXvshR/meJ1BiqaRARKfvJcj4eMSpiUvhDe1QFXXjfXRdetJcknyJ8Pv6v10G/OV3ELYwdx2dYL5C+Ao4qj9QCjoD8bb/juCjtZoSxMncbm4T7ORbXs/Ulx+TEuOUmRAjxr+zaWzzO7ZfYJMhIPz+LSixKpaVmxP89DEK5LJ1T6jA50QmPft6AbOMuoq99haWH9lMgqrIfBB+ZNNHSEE9PwMUxhX/TQP3oJgmrnZcMV4nBOcovbWM5s+odu2mvzoAcRMHVxEztASbMBwdJ7amJvoRRrXTtp642FQHAe8pFPlW14x1hShXAO4YfYkmmAhLqlujammuQ6bRg7XjBvh0zE88i5UBRiRsH+MV35u3c0ciugUmbrAZ0u9Yfv19MNSMVNMsREDuxQ==

View file

@ -1 +1 @@
{"encrypted":true,"first_run":true,"entries":[{"encryption_iv":"xLHUZJzinEfUwLzEzqfI7Q==","encryption_salt":"bcp2z6P88A0=","filename":"1234.maFile","steamid":1234}],"periodic_checking":false,"periodic_checking_interval":5,"periodic_checking_checkall":false,"auto_confirm_market_transactions":false,"auto_confirm_trades":false} {"encrypted":true,"first_run":true,"entries":[{"encryption_iv":"ifChnv66eA+/dYqGsQMIOA==","encryption_salt":"O2K8FAOWK9c=","filename":"1234.maFile","steamid":1234}],"periodic_checking":false,"periodic_checking_interval":5,"periodic_checking_checkall":false,"auto_confirm_market_transactions":false,"auto_confirm_trades":false}

View file

@ -1 +1 @@
{"shared_secret":"secret1234","serial_number":"kljasfhds","revocation_code":"R12345","uri":"otpauth://totp/Steam:example?secret=ASDF&issuer=Steam","server_time":1602522478,"account_name":"example","token_gid":"jkkjlhkhjgf","identity_secret":"kjsdlwowiqe=","secret_1":"sklduhfgsdlkjhf=","status":1,"device_id":"android:99d2ad0e-4bad-4247-b111-26393aae0be3","fully_enrolled":true,"Session":{"SessionID":"a;lskdjf","SteamLogin":"983498437543","SteamLoginSecure":"dlkjdsl;j%7C%32984730298","WebCookie":";lkjsed;klfjas98093","OAuthToken":"asdk;lf;dsjlkfd","SteamID":1234}} {"shared_secret":"zvIayp3JPvtvX/QGHqsqKBk/44s=","serial_number":"kljasfhds","revocation_code":"R12345","uri":"otpauth://totp/Steam:example?secret=ASDF&issuer=Steam","server_time":1602522478,"account_name":"example","token_gid":"jkkjlhkhjgf","identity_secret":"kjsdlwowiqe=","secret_1":"sklduhfgsdlkjhf=","status":1,"device_id":"android:99d2ad0e-4bad-4247-b111-26393aae0be3","fully_enrolled":true,"Session":{"SessionID":"a;lskdjf","SteamLogin":"983498437543","SteamLoginSecure":"dlkjdsl;j%7C%32984730298","WebCookie":";lkjsed;klfjas98093","OAuthToken":"asdk;lf;dsjlkfd","SteamID":1234}}

View file

@ -24,3 +24,4 @@ log = "0.4.14"
scraper = "0.12.0" scraper = "0.12.0"
maplit = "1.0.2" maplit = "1.0.2"
thiserror = "1.0.26" thiserror = "1.0.26"
secrecy = { version = "0.8", features = ["serde"] }

View file

@ -1,3 +1,4 @@
use crate::token::TwoFactorSecret;
pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError}; pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError};
use anyhow::Result; use anyhow::Result;
pub use confirmation::{Confirmation, ConfirmationType}; pub use confirmation::{Confirmation, ConfirmationType};
@ -24,6 +25,7 @@ extern crate maplit;
mod accountlinker; mod accountlinker;
mod confirmation; mod confirmation;
pub mod steamapi; pub mod steamapi;
pub mod token;
mod userlogin; mod userlogin;
// const STEAMAPI_BASE: String = "https://api.steampowered.com"; // const STEAMAPI_BASE: String = "https://api.steampowered.com";
@ -42,7 +44,7 @@ pub struct SteamGuardAccount {
pub account_name: String, pub account_name: String,
pub serial_number: String, pub serial_number: String,
pub revocation_code: String, pub revocation_code: String,
pub shared_secret: String, pub shared_secret: TwoFactorSecret,
pub token_gid: String, pub token_gid: String,
pub identity_secret: String, pub identity_secret: String,
pub server_time: u64, pub server_time: u64,
@ -58,12 +60,6 @@ fn build_time_bytes(time: i64) -> [u8; 8] {
return time.to_be_bytes(); return time.to_be_bytes();
} }
pub fn parse_shared_secret(secret: String) -> anyhow::Result<[u8; 20]> {
ensure!(secret.len() != 0, "unable to parse empty shared secret");
let result = base64::decode(secret)?.try_into();
return Ok(result.unwrap());
}
fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String { fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String {
let decode: &[u8] = &base64::decode(&identity_secret).unwrap(); let decode: &[u8] = &base64::decode(&identity_secret).unwrap();
let time_bytes = build_time_bytes(time); let time_bytes = build_time_bytes(time);
@ -80,7 +76,7 @@ impl SteamGuardAccount {
account_name: String::from(""), account_name: String::from(""),
serial_number: String::from(""), serial_number: String::from(""),
revocation_code: String::from(""), revocation_code: String::from(""),
shared_secret: String::from(""), shared_secret: TwoFactorSecret::new(),
token_gid: String::from(""), token_gid: String::from(""),
identity_secret: String::from(""), identity_secret: String::from(""),
server_time: 0, server_time: 0,
@ -93,29 +89,7 @@ impl SteamGuardAccount {
} }
pub fn generate_code(&self, time: i64) -> String { pub fn generate_code(&self, time: i64) -> String {
let steam_guard_code_translations: [u8; 26] = [ return self.shared_secret.generate_code(time);
50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84,
86, 87, 88, 89,
];
// this effectively makes it so that it creates a new code every 30 seconds.
let time_bytes: [u8; 8] = build_time_bytes(time / 30i64);
let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()).unwrap();
let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes);
let mut code_array: [u8; 5] = [0; 5];
let b = (hashed_data[19] & 0xF) as usize;
let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24
| ((hashed_data[b + 1] & 0xFF) as i32) << 16
| ((hashed_data[b + 2] & 0xFF) as i32) << 8
| ((hashed_data[b + 3] & 0xFF) as i32);
for i in 0..5 {
code_array[i] = steam_guard_code_translations
[code_point as usize % steam_guard_code_translations.len()];
code_point /= steam_guard_code_translations.len() as i32;
}
return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap();
} }
fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> { fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> {
@ -306,26 +280,6 @@ fn parse_confirmations(text: String) -> anyhow::Result<Vec<Confirmation>> {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_build_time_bytes() {
let t1 = build_time_bytes(1617591917i64);
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
assert!(
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
"Arrays are not equal, got {:?}",
t1
);
}
#[test]
fn test_generate_code() {
let mut account = SteamGuardAccount::new();
account.shared_secret = String::from("zvIayp3JPvtvX/QGHqsqKBk/44s=");
let code = account.generate_code(1616374841i64);
assert_eq!(code, "2F9J5")
}
#[test] #[test]
fn test_generate_confirmation_hash_for_time() { fn test_generate_confirmation_hash_for_time() {
assert_eq!( assert_eq!(

View file

@ -1,3 +1,5 @@
use crate::token::TwoFactorSecret;
use crate::SteamGuardAccount;
use log::*; use log::*;
use reqwest::{ use reqwest::{
blocking::RequestBuilder, blocking::RequestBuilder,
@ -12,8 +14,6 @@ use std::iter::FromIterator;
use std::str::FromStr; use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use crate::SteamGuardAccount;
lazy_static! { lazy_static! {
static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::<Url>().unwrap(); static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::<Url>().unwrap();
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into(); static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
@ -609,9 +609,9 @@ pub struct AddAuthenticatorResponse {
} }
impl AddAuthenticatorResponse { impl AddAuthenticatorResponse {
pub fn to_steam_guard_account(&self) -> SteamGuardAccount { pub fn to_steam_guard_account(self) -> SteamGuardAccount {
SteamGuardAccount { SteamGuardAccount {
shared_secret: self.shared_secret.clone(), shared_secret: TwoFactorSecret::parse_shared_secret(self.shared_secret).unwrap(),
serial_number: self.serial_number.clone(), serial_number: self.serial_number.clone(),
revocation_code: self.revocation_code.clone(), revocation_code: self.revocation_code.clone(),
uri: self.uri.clone(), uri: self.uri.clone(),

150
steamguard/src/token.rs Normal file
View file

@ -0,0 +1,150 @@
use secrecy::{ExposeSecret, Secret};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::convert::TryInto;
#[derive(Debug, Clone)]
pub struct TwoFactorSecret(Secret<[u8; 20]>);
// pub struct TwoFactorSecret(Secret<Vec<u8>>);
impl TwoFactorSecret {
pub fn new() -> Self {
return Self([0u8; 20].into());
}
pub fn parse_shared_secret(secret: String) -> anyhow::Result<Self> {
ensure!(secret.len() != 0, "unable to parse empty shared secret");
let result: [u8; 20] = base64::decode(secret)?.try_into().unwrap();
return Ok(Self(result.into()));
}
/// Generate a 5 character 2FA code to that can be used to log in to Steam.
pub fn generate_code(&self, time: i64) -> String {
let steam_guard_code_translations: [u8; 26] = [
50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84,
86, 87, 88, 89,
];
// this effectively makes it so that it creates a new code every 30 seconds.
let time_bytes: [u8; 8] = build_time_bytes(time / 30i64);
let hashed_data = hmacsha1::hmac_sha1(self.0.expose_secret(), &time_bytes);
let mut code_array: [u8; 5] = [0; 5];
let b = (hashed_data[19] & 0xF) as usize;
let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24
| ((hashed_data[b + 1] & 0xFF) as i32) << 16
| ((hashed_data[b + 2] & 0xFF) as i32) << 8
| ((hashed_data[b + 3] & 0xFF) as i32);
for i in 0..5 {
code_array[i] = steam_guard_code_translations
[code_point as usize % steam_guard_code_translations.len()];
code_point /= steam_guard_code_translations.len() as i32;
}
return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap();
}
}
impl Serialize for TwoFactorSecret {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(base64::encode(&self.0.expose_secret()).as_str())
}
}
impl<'de> Deserialize<'de> for TwoFactorSecret {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(TwoFactorSecret::parse_shared_secret(String::deserialize(deserializer)?).unwrap())
}
}
impl PartialEq for TwoFactorSecret {
fn eq(&self, other: &Self) -> bool {
return self.0.expose_secret() == other.0.expose_secret();
}
}
impl Eq for TwoFactorSecret {}
fn build_time_bytes(time: i64) -> [u8; 8] {
return time.to_be_bytes();
}
mod tests {
use super::*;
#[test]
fn test_serialize() -> anyhow::Result<()> {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct FooBar {
secret: TwoFactorSecret,
}
let secret = FooBar {
secret: TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?,
};
let serialized = serde_json::to_string(&secret)?;
assert_eq!(serialized, "{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}");
return Ok(());
}
#[test]
fn test_deserialize() -> anyhow::Result<()> {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct FooBar {
secret: TwoFactorSecret,
}
let secret: FooBar =
serde_json::from_str(&"{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}")?;
let code = secret.secret.generate_code(1616374841i64);
assert_eq!(code, "2F9J5");
return Ok(());
}
#[test]
fn test_serialize_and_deserialize() -> anyhow::Result<()> {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct FooBar {
secret: TwoFactorSecret,
}
let secret = FooBar {
secret: TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?,
};
let serialized = serde_json::to_string(&secret)?;
let deserialized: FooBar = serde_json::from_str(&serialized)?;
assert_eq!(deserialized, secret);
return Ok(());
}
#[test]
fn test_build_time_bytes() {
let t1 = build_time_bytes(1617591917i64);
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
assert!(
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
"Arrays are not equal, got {:?}",
t1
);
}
#[test]
fn test_generate_code() -> anyhow::Result<()> {
let secret = TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?;
let code = secret.generate_code(1616374841i64);
assert_eq!(code, "2F9J5");
return Ok(());
}
}