From 8930284cd794e3e8fe60c37f3ad06fddf6f00a4f Mon Sep 17 00:00:00 2001 From: Bilal Elmoussaoui Date: Mon, 10 Apr 2023 01:38:17 +0200 Subject: [PATCH] secrets: Zeroize on drop --- Cargo.lock | 1 + Cargo.toml | 1 + src/backup/aegis.rs | 9 ++- src/backup/andotp.rs | 16 ++++- src/backup/bitwarden.rs | 4 +- src/backup/freeotp_json.rs | 10 ++- src/backup/google.rs | 14 +++- src/backup/legacy.rs | 11 ++- src/models/account.rs | 24 +++---- src/models/mod.rs | 2 + src/models/otp.rs | 134 +++++++++++++------------------------ src/models/otp_uri.rs | 11 ++- src/models/token.rs | 56 ++++++++++++++++ 13 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 src/models/token.rs diff --git a/Cargo.lock b/Cargo.lock index 3d70a12..ba3b294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,7 @@ dependencies = [ "url", "uuid", "zbar-rust", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 04fd285..88b3bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,4 @@ unicase = "2.6" url = "2.2" uuid = {version = "1.0", features = ["v4"]} zbar-rust = "0.0" +zeroize = {version = "1", features = ["zeroize_derive"]} diff --git a/src/backup/aegis.rs b/src/backup/aegis.rs index 344ed83..6bee543 100644 --- a/src/backup/aegis.rs +++ b/src/backup/aegis.rs @@ -17,6 +17,7 @@ use gettextrs::gettext; use gtk::prelude::*; use rand::RngCore; use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::{Backupable, Restorable, RestorableItem}; use crate::models::{Account, Algorithm, Method, Provider, ProvidersModel}; @@ -301,7 +302,7 @@ impl Item { // First, create a detail struct let detail = Detail { - secret: account.token(), + secret: account.token().as_string(), algorithm: provider.algorithm(), digits: provider.digits(), // TODO should be none for hotp @@ -335,13 +336,17 @@ impl Item { } /// OTP Entry Details -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] pub struct Detail { pub secret: String, #[serde(rename = "algo")] + #[zeroize(skip)] pub algorithm: Algorithm, + #[zeroize(skip)] pub digits: u32, + #[zeroize(skip)] pub period: Option, + #[zeroize(skip)] pub counter: Option, } diff --git a/src/backup/andotp.rs b/src/backup/andotp.rs index a73eb83..e0f2ecd 100644 --- a/src/backup/andotp.rs +++ b/src/backup/andotp.rs @@ -2,25 +2,37 @@ use anyhow::Result; use gettextrs::gettext; use gtk::prelude::*; use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::{Backupable, Restorable, RestorableItem}; use crate::models::{Account, Algorithm, Method, Provider, ProvidersModel}; #[allow(clippy::upper_case_acronyms)] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] pub struct AndOTP { pub secret: String, + #[zeroize(skip)] pub issuer: String, + #[zeroize(skip)] pub label: String, + #[zeroize(skip)] pub digits: u32, #[serde(rename = "type")] + #[zeroize(skip)] pub method: Method, + #[zeroize(skip)] pub algorithm: Algorithm, + #[zeroize(skip)] pub thumbnail: Option, + #[zeroize(skip)] pub last_used: i64, + #[zeroize(skip)] pub used_frequency: i32, + #[zeroize(skip)] pub counter: Option, + #[zeroize(skip)] pub tags: Vec, + #[zeroize(skip)] pub period: Option, } @@ -82,7 +94,7 @@ impl Backupable for AndOTP { let account = accounts.item(j).and_downcast::().unwrap(); let otp_item = AndOTP { - secret: account.token(), + secret: account.token().as_string(), issuer: provider.name(), label: account.name(), digits: provider.digits(), diff --git a/src/backup/bitwarden.rs b/src/backup/bitwarden.rs index 8385268..5baff66 100644 --- a/src/backup/bitwarden.rs +++ b/src/backup/bitwarden.rs @@ -1,6 +1,7 @@ use anyhow::Result; use gettextrs::gettext; use serde::Deserialize; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::{Restorable, RestorableItem}; use crate::models::{ @@ -30,8 +31,9 @@ pub struct BitwardenItem { counter: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, ZeroizeOnDrop, Zeroize)] struct BitwardenDetails { + #[zeroize(skip)] username: Option, totp: Option, } diff --git a/src/backup/freeotp_json.rs b/src/backup/freeotp_json.rs index 3381087..a927c3f 100644 --- a/src/backup/freeotp_json.rs +++ b/src/backup/freeotp_json.rs @@ -1,6 +1,7 @@ use anyhow::Result; use gettextrs::gettext; use serde::Deserialize; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::{Restorable, RestorableItem}; use crate::models::{otp::encode_secret, Algorithm, Method}; @@ -10,18 +11,25 @@ pub struct FreeOTPJSON { tokens: Vec, } -#[derive(Deserialize)] +#[derive(Deserialize, Zeroize, ZeroizeOnDrop)] pub struct FreeOTPItem { + #[zeroize(skip)] algo: Algorithm, // Note: For some reason FreeOTP adds -1 to the counter + #[zeroize(skip)] counter: Option, + #[zeroize(skip)] digits: Option, + #[zeroize(skip)] label: String, #[serde(rename = "issuerExt")] + #[zeroize(skip)] issuer: String, + #[zeroize(skip)] period: Option, secret: Vec, #[serde(rename = "type")] + #[zeroize(skip)] method: Method, } diff --git a/src/backup/google.rs b/src/backup/google.rs index 0e6fec7..ace614d 100644 --- a/src/backup/google.rs +++ b/src/backup/google.rs @@ -94,8 +94,8 @@ impl Restorable for Google { .trim_end_matches(|c| c == '\0' || c == '=') .to_owned() }, - label: otp.name, - issuer: otp.issuer, + label: otp.name.clone(), + issuer: otp.issuer.clone(), period: None, counter: Some(otp.counter as u32), }); @@ -128,6 +128,8 @@ mod protobuf { } pub mod migration_payload { + use zeroize::{Zeroize, ZeroizeOnDrop}; + use super::*; #[derive(Debug, Enumeration)] @@ -143,21 +145,27 @@ mod protobuf { OTP_TOTP = 2, } - #[derive(Message)] + #[derive(Message, Zeroize, ZeroizeOnDrop)] pub struct OtpParameters { #[prost(bytes)] pub secret: Vec, + #[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, } } diff --git a/src/backup/legacy.rs b/src/backup/legacy.rs index 33d74fa..9dfa5d9 100644 --- a/src/backup/legacy.rs +++ b/src/backup/legacy.rs @@ -1,22 +1,31 @@ use anyhow::Result; use gettextrs::gettext; use serde::Deserialize; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::{Restorable, RestorableItem}; use crate::models::{Algorithm, Method}; // Same as andOTP except uses the first tag for the issuer -#[derive(Deserialize)] +#[derive(Deserialize, Zeroize, ZeroizeOnDrop)] pub struct LegacyAuthenticator { pub secret: String, + #[zeroize(skip)] pub label: String, + #[zeroize(skip)] pub digits: u32, #[serde(rename = "type")] + #[zeroize(skip)] pub method: Method, + #[zeroize(skip)] pub algorithm: Algorithm, + #[zeroize(skip)] pub thumbnail: String, + #[zeroize(skip)] pub last_used: i64, + #[zeroize(skip)] pub tags: Vec, + #[zeroize(skip)] pub period: u32, } diff --git a/src/models/account.rs b/src/models/account.rs index d23ddec..3daa83f 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -9,6 +9,7 @@ use gtk::{ }; use unicase::UniCase; +use super::Token; use crate::{ models::{database, keyring, otp, DieselProvider, Method, OTPUri, Provider, RUNTIME}, schema::accounts, @@ -44,6 +45,7 @@ mod imp { use once_cell::sync::{Lazy, OnceCell}; use super::*; + use crate::models::Token; #[derive(glib::Properties)] #[properties(wrapper_type = super::Account)] @@ -56,7 +58,7 @@ mod imp { pub name: RefCell, #[property(get, set = Self::set_counter, default = otp::HOTP_DEFAULT_COUNTER)] pub counter: Cell, - pub token: OnceCell, + pub token: OnceCell, #[property(get, set, construct_only)] pub token_id: RefCell, // We don't use property here as we can't mark the getter as not nullable @@ -254,7 +256,7 @@ impl Account { .property("id", id) .property("name", name) .property("token-id", token_id) - .property("provider", provider) + .property("provider", provider.clone()) .property("counter", counter) .build(); @@ -268,6 +270,7 @@ impl Account { }) })? }; + let token = Token::from_str(&token, provider.algorithm(), provider.digits())?; account.imp().token.set(token).unwrap(); account.generate_otp(); Ok(account) @@ -283,17 +286,8 @@ impl Account { }; let otp_password: Result = match provider.method() { - Method::Steam => otp::steam(&self.token(), counter), - _ => { - let token = otp::hotp( - &self.token(), - counter, - provider.algorithm(), - provider.digits(), - ); - - token.map(|d| otp::format(d, provider.digits() as usize)) - } + Method::Steam => self.token().steam(counter), + _ => self.token().hotp_formatted(counter), }; let label = match otp_password { @@ -351,8 +345,8 @@ impl Account { Ok(()) } - pub fn token(&self) -> String { - self.imp().token.get().unwrap().clone() + pub fn token(&self) -> &Token { + self.imp().token.get().unwrap() } pub fn otp_uri(&self) -> OTPUri { diff --git a/src/models/mod.rs b/src/models/mod.rs index 00330bd..964dc66 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -14,6 +14,7 @@ mod provider_sorter; mod providers; mod search_provider; mod settings; +mod token; pub static RUNTIME: Lazy = Lazy::new(|| tokio::runtime::Runtime::new().unwrap()); @@ -36,4 +37,5 @@ pub use self::{ providers::ProvidersModel, search_provider::{start, SearchProviderAction}, settings::Settings, + token::Token, }; diff --git a/src/models/otp.rs b/src/models/otp.rs index 74c12a4..546116a 100644 --- a/src/models/otp.rs +++ b/src/models/otp.rs @@ -21,7 +21,7 @@ pub static TOTP_DEFAULT_PERIOD: u32 = 30; /// Decodes a secret (given as an RFC4648 base32-encoded ASCII string) /// into a byte string. It fails if secret is not a valid Base32 string. -fn decode_secret(secret: &str) -> Result> { +pub fn decode_secret(secret: &str) -> Result> { let secret = secret.trim().replace(' ', "").to_ascii_uppercase(); // The buffer should have a length of secret.len() * 5 / 8. BASE32_NOPAD @@ -60,15 +60,13 @@ fn encode_digest(digest: &[u8]) -> Result { /// Performs the [HMAC-based One-time Password Algorithm](http://en.wikipedia.org/wiki/HMAC-based_One-time_Password_Algorithm) /// (HOTP) given an RFC4648 base32 encoded secret, and an integer counter. -pub(crate) fn hotp(secret: &str, counter: u64, algorithm: Algorithm, digits: u32) -> Result { - let decoded = decode_secret(secret)?; - let digest = encode_digest(calc_digest(&decoded, counter, algorithm).as_ref())?; +pub(crate) fn hotp(secret: &[u8], counter: u64, algorithm: Algorithm, digits: u32) -> Result { + let digest = encode_digest(calc_digest(secret, counter, algorithm).as_ref())?; Ok(digest % 10_u32.pow(digits)) } -pub(crate) fn steam(secret: &str, counter: u64) -> Result { - let decoded = decode_secret(secret)?; - let mut full_token = encode_digest(calc_digest(&decoded, counter, Algorithm::SHA1).as_ref())?; +pub(crate) fn steam(secret: &[u8], counter: u64) -> Result { + let mut full_token = encode_digest(calc_digest(secret, counter, Algorithm::SHA1).as_ref())?; let mut code = String::new(); let total_chars = STEAM_CHARS.len() as u32; @@ -103,164 +101,126 @@ pub(crate) fn time_based_counter(period: u32) -> u64 { #[cfg(test)] mod tests { - use super::{ - encode_secret, format, hotp, steam, Algorithm, DEFAULT_DIGITS, TOTP_DEFAULT_PERIOD, - }; + use super::{format, hotp, Algorithm, DEFAULT_DIGITS, TOTP_DEFAULT_PERIOD}; + use crate::models::Token; #[test] fn test_totp() { - let secret_sha1 = encode_secret(b"12345678901234567890"); - let secret_sha256 = encode_secret(b"12345678901234567890123456789012"); - let secret_sha512 = - encode_secret(b"1234567890123456789012345678901234567890123456789012345678901234"); + let secret_sha1 = b"12345678901234567890"; + let secret_sha256 = b"12345678901234567890123456789012"; + let secret_sha512 = b"1234567890123456789012345678901234567890123456789012345678901234"; let counter1 = 59 / TOTP_DEFAULT_PERIOD as u64; assert_eq!( Some(94287082), - hotp(&secret_sha1, counter1, Algorithm::SHA1, 8).ok() + hotp(secret_sha1, counter1, Algorithm::SHA1, 8).ok() ); assert_eq!( Some(46119246), - hotp(&secret_sha256, counter1, Algorithm::SHA256, 8).ok() + hotp(secret_sha256, counter1, Algorithm::SHA256, 8).ok() ); assert_eq!( Some(90693936), - hotp(&secret_sha512, counter1, Algorithm::SHA512, 8).ok() + hotp(secret_sha512, counter1, Algorithm::SHA512, 8).ok() ); let counter2 = 1111111109 / TOTP_DEFAULT_PERIOD as u64; assert_eq!( Some(7081804), - hotp(&secret_sha1, counter2, Algorithm::SHA1, 8).ok() + hotp(secret_sha1, counter2, Algorithm::SHA1, 8).ok() ); assert_eq!( Some(68084774), - hotp(&secret_sha256, counter2, Algorithm::SHA256, 8).ok() + hotp(secret_sha256, counter2, Algorithm::SHA256, 8).ok() ); assert_eq!( Some(25091201), - hotp(&secret_sha512, counter2, Algorithm::SHA512, 8).ok() + hotp(secret_sha512, counter2, Algorithm::SHA512, 8).ok() ); let counter3 = 1111111111 / TOTP_DEFAULT_PERIOD as u64; assert_eq!( Some(14050471), - hotp(&secret_sha1, counter3, Algorithm::SHA1, 8).ok() + hotp(secret_sha1, counter3, Algorithm::SHA1, 8).ok() ); assert_eq!( Some(67062674), - hotp(&secret_sha256, counter3, Algorithm::SHA256, 8).ok() + hotp(secret_sha256, counter3, Algorithm::SHA256, 8).ok() ); assert_eq!( Some(99943326), - hotp(&secret_sha512, counter3, Algorithm::SHA512, 8).ok() + hotp(secret_sha512, counter3, Algorithm::SHA512, 8).ok() ); let counter4 = 1234567890 / TOTP_DEFAULT_PERIOD as u64; assert_eq!( Some(89005924), - hotp(&secret_sha1, counter4, Algorithm::SHA1, 8).ok() + hotp(secret_sha1, counter4, Algorithm::SHA1, 8).ok() ); assert_eq!( Some(91819424), - hotp(&secret_sha256, counter4, Algorithm::SHA256, 8).ok() + hotp(secret_sha256, counter4, Algorithm::SHA256, 8).ok() ); assert_eq!( Some(93441116), - hotp(&secret_sha512, counter4, Algorithm::SHA512, 8).ok() + hotp(secret_sha512, counter4, Algorithm::SHA512, 8).ok() ); let counter5 = 2000000000 / TOTP_DEFAULT_PERIOD as u64; assert_eq!( Some(69279037), - hotp(&secret_sha1, counter5, Algorithm::SHA1, 8).ok() + hotp(secret_sha1, counter5, Algorithm::SHA1, 8).ok() ); assert_eq!( Some(90698825), - hotp(&secret_sha256, counter5, Algorithm::SHA256, 8).ok() + hotp(secret_sha256, counter5, Algorithm::SHA256, 8).ok() ); assert_eq!( Some(38618901), - hotp(&secret_sha512, counter5, Algorithm::SHA512, 8).ok() + hotp(secret_sha512, counter5, Algorithm::SHA512, 8).ok() ); let counter6 = 20000000000 / TOTP_DEFAULT_PERIOD as u64; assert_eq!( Some(65353130), - hotp(&secret_sha1, counter6, Algorithm::SHA1, 8).ok() + hotp(secret_sha1, counter6, Algorithm::SHA1, 8).ok() ); assert_eq!( Some(77737706), - hotp(&secret_sha256, counter6, Algorithm::SHA256, 8).ok() + hotp(secret_sha256, counter6, Algorithm::SHA256, 8).ok() ); assert_eq!( Some(47863826), - hotp(&secret_sha512, counter6, Algorithm::SHA512, 8).ok() + hotp(secret_sha512, counter6, Algorithm::SHA512, 8).ok() ); } // Some of the tests are heavily inspired(copy-paste) of the andOTP application #[test] fn test_hotp() { - assert_eq!( - hotp("BASE32SECRET3232", 0, Algorithm::SHA1, DEFAULT_DIGITS).ok(), - Some(260182) - ); - assert_eq!( - hotp("BASE32SECRET3232", 1, Algorithm::SHA1, DEFAULT_DIGITS).ok(), - Some(55283) - ); - assert_eq!( - hotp("BASE32SECRET3232", 1401, Algorithm::SHA1, DEFAULT_DIGITS).ok(), - Some(316439) - ); - let secret = encode_secret(b"12345678901234567890"); - assert_eq!( - Some(755224), - hotp(&secret, 0, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(287082), - hotp(&secret, 1, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(359152), - hotp(&secret, 2, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(969429), - hotp(&secret, 3, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(338314), - hotp(&secret, 4, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(254676), - hotp(&secret, 5, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(287922), - hotp(&secret, 6, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(162583), - hotp(&secret, 7, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(399871), - hotp(&secret, 8, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); - assert_eq!( - Some(520489), - hotp(&secret, 9, Algorithm::SHA1, DEFAULT_DIGITS).ok() - ); + let token = Token::from_str("BASE32SECRET3232", Algorithm::SHA1, DEFAULT_DIGITS).unwrap(); + assert_eq!(token.hotp(0).ok(), Some(260182)); + assert_eq!(token.hotp(1).ok(), Some(55283)); + assert_eq!(token.hotp(1401).ok(), Some(316439)); + + let token = Token::from_bytes(b"12345678901234567890", Algorithm::SHA1, DEFAULT_DIGITS); + assert_eq!(Some(755224), token.hotp(0).ok(),); + assert_eq!(Some(287082), token.hotp(1).ok()); + assert_eq!(Some(359152), token.hotp(2).ok()); + assert_eq!(Some(969429), token.hotp(3).ok()); + assert_eq!(Some(338314), token.hotp(4).ok()); + assert_eq!(Some(254676), token.hotp(5).ok()); + assert_eq!(Some(287922), token.hotp(6).ok()); + assert_eq!(Some(162583), token.hotp(7).ok()); + assert_eq!(Some(399871), token.hotp(8).ok()); + assert_eq!(Some(520489), token.hotp(9).ok()); } #[test] fn test_steam() { - assert_eq!(steam("BASE32SECRET3232", 0).ok(), Some("2TC8B".into())); - assert_eq!(steam("BASE32SECRET3232", 1).ok(), Some("YKKK4".into())); + let token = Token::from_str_steam("BASE32SECRET3232").unwrap(); + assert_eq!(token.steam(0).ok(), Some("2TC8B".into())); + assert_eq!(token.steam(1).ok(), Some("YKKK4".into())); } #[test] diff --git a/src/models/otp_uri.rs b/src/models/otp_uri.rs index 0b5c064..2e6eda2 100644 --- a/src/models/otp_uri.rs +++ b/src/models/otp_uri.rs @@ -2,6 +2,7 @@ use std::{fmt::Write, str::FromStr}; use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC}; use url::Url; +use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ backup::RestorableItem, @@ -9,14 +10,22 @@ use crate::{ }; #[allow(clippy::upper_case_acronyms)] +#[derive(Zeroize, ZeroizeOnDrop)] pub struct OTPUri { + #[zeroize(skip)] pub(crate) algorithm: Algorithm, + #[zeroize(skip)] pub(crate) label: String, pub(crate) secret: String, + #[zeroize(skip)] pub(crate) issuer: String, + #[zeroize(skip)] pub(crate) method: Method, + #[zeroize(skip)] pub(crate) digits: Option, + #[zeroize(skip)] pub(crate) period: Option, + #[zeroize(skip)] pub(crate) counter: Option, } @@ -180,7 +189,7 @@ impl From<&Account> for OTPUri { Self { method: a.provider().method(), label: a.name(), - secret: a.token(), + secret: a.token().as_string(), issuer: a.provider().name(), algorithm: a.provider().algorithm(), digits: Some(a.provider().digits()), diff --git a/src/models/token.rs b/src/models/token.rs new file mode 100644 index 0000000..2fc8cdb --- /dev/null +++ b/src/models/token.rs @@ -0,0 +1,56 @@ +use anyhow::Result; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use super::{ + otp::{self, STEAM_DEFAULT_DIGITS}, + Algorithm, +}; + +#[derive(Debug, Zeroize, ZeroizeOnDrop)] +pub struct Token { + secret: Vec, + #[zeroize(skip)] + algorithm: Algorithm, + #[zeroize(skip)] + digits: u32, +} + +impl Token { + pub fn from_bytes_steam(secret: &[u8]) -> Self { + Self::from_bytes(secret, Algorithm::SHA1, STEAM_DEFAULT_DIGITS) + } + + pub fn from_str_steam(secret: &str) -> Result { + Self::from_str(secret, Algorithm::SHA1, STEAM_DEFAULT_DIGITS) + } + + pub fn from_str(secret: &str, algorithm: Algorithm, digits: u32) -> Result { + let decoded = otp::decode_secret(secret)?; + Ok(Self::from_bytes(&decoded, algorithm, digits)) + } + + pub fn from_bytes(secret: &[u8], algorithm: Algorithm, digits: u32) -> Self { + Self { + secret: secret.to_owned(), + algorithm, + digits, + } + } + + pub fn hotp(&self, counter: u64) -> Result { + otp::hotp(&self.secret, counter, self.algorithm, self.digits) + } + + pub fn hotp_formatted(&self, counter: u64) -> Result { + self.hotp(counter) + .map(|d| otp::format(d, self.digits as usize)) + } + + pub fn steam(&self, counter: u64) -> Result { + otp::steam(&self.secret, counter) + } + + pub fn as_string(&self) -> String { + otp::encode_secret(&self.secret) + } +}