mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 00:34:40 +01:00
secrets: Zeroize on drop
This commit is contained in:
parent
ba4b10ab71
commit
8930284cd7
13 changed files with 180 additions and 113 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -181,6 +181,7 @@ dependencies = [
|
|||
"url",
|
||||
"uuid",
|
||||
"zbar-rust",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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"]}
|
||||
|
|
|
@ -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<u32>,
|
||||
#[zeroize(skip)]
|
||||
pub counter: Option<u32>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
#[zeroize(skip)]
|
||||
pub last_used: i64,
|
||||
#[zeroize(skip)]
|
||||
pub used_frequency: i32,
|
||||
#[zeroize(skip)]
|
||||
pub counter: Option<u32>,
|
||||
#[zeroize(skip)]
|
||||
pub tags: Vec<String>,
|
||||
#[zeroize(skip)]
|
||||
pub period: Option<u32>,
|
||||
}
|
||||
|
||||
|
@ -82,7 +94,7 @@ impl Backupable for AndOTP {
|
|||
let account = accounts.item(j).and_downcast::<Account>().unwrap();
|
||||
|
||||
let otp_item = AndOTP {
|
||||
secret: account.token(),
|
||||
secret: account.token().as_string(),
|
||||
issuer: provider.name(),
|
||||
label: account.name(),
|
||||
digits: provider.digits(),
|
||||
|
|
|
@ -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<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ZeroizeOnDrop, Zeroize)]
|
||||
struct BitwardenDetails {
|
||||
#[zeroize(skip)]
|
||||
username: Option<String>,
|
||||
totp: Option<String>,
|
||||
}
|
||||
|
|
|
@ -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<FreeOTPItem>,
|
||||
}
|
||||
|
||||
#[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<u32>,
|
||||
#[zeroize(skip)]
|
||||
digits: Option<u32>,
|
||||
#[zeroize(skip)]
|
||||
label: String,
|
||||
#[serde(rename = "issuerExt")]
|
||||
#[zeroize(skip)]
|
||||
issuer: String,
|
||||
#[zeroize(skip)]
|
||||
period: Option<u32>,
|
||||
secret: Vec<i16>,
|
||||
#[serde(rename = "type")]
|
||||
#[zeroize(skip)]
|
||||
method: Method,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
#[zeroize(skip)]
|
||||
pub period: u32,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
#[property(get, set = Self::set_counter, default = otp::HOTP_DEFAULT_COUNTER)]
|
||||
pub counter: Cell<u32>,
|
||||
pub token: OnceCell<String>,
|
||||
pub token: OnceCell<Token>,
|
||||
#[property(get, set, construct_only)]
|
||||
pub token_id: RefCell<String>,
|
||||
// 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<String> = 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 {
|
||||
|
|
|
@ -14,6 +14,7 @@ mod provider_sorter;
|
|||
mod providers;
|
||||
mod search_provider;
|
||||
mod settings;
|
||||
mod token;
|
||||
|
||||
pub static RUNTIME: Lazy<tokio::runtime::Runtime> =
|
||||
Lazy::new(|| tokio::runtime::Runtime::new().unwrap());
|
||||
|
@ -36,4 +37,5 @@ pub use self::{
|
|||
providers::ProvidersModel,
|
||||
search_provider::{start, SearchProviderAction},
|
||||
settings::Settings,
|
||||
token::Token,
|
||||
};
|
||||
|
|
|
@ -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<Vec<u8>> {
|
||||
pub fn decode_secret(secret: &str) -> Result<Vec<u8>> {
|
||||
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<u32> {
|
|||
|
||||
/// 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<u32> {
|
||||
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<u32> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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]
|
||||
|
|
|
@ -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<u32>,
|
||||
#[zeroize(skip)]
|
||||
pub(crate) period: Option<u32>,
|
||||
#[zeroize(skip)]
|
||||
pub(crate) counter: Option<u32>,
|
||||
}
|
||||
|
||||
|
@ -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()),
|
||||
|
|
56
src/models/token.rs
Normal file
56
src/models/token.rs
Normal file
|
@ -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<u8>,
|
||||
#[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> {
|
||||
Self::from_str(secret, Algorithm::SHA1, STEAM_DEFAULT_DIGITS)
|
||||
}
|
||||
|
||||
pub fn from_str(secret: &str, algorithm: Algorithm, digits: u32) -> Result<Self> {
|
||||
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<u32> {
|
||||
otp::hotp(&self.secret, counter, self.algorithm, self.digits)
|
||||
}
|
||||
|
||||
pub fn hotp_formatted(&self, counter: u64) -> Result<String> {
|
||||
self.hotp(counter)
|
||||
.map(|d| otp::format(d, self.digits as usize))
|
||||
}
|
||||
|
||||
pub fn steam(&self, counter: u64) -> Result<String> {
|
||||
otp::steam(&self.secret, counter)
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> String {
|
||||
otp::encode_secret(&self.secret)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue