secrets: Zeroize on drop

This commit is contained in:
Bilal Elmoussaoui 2023-04-10 01:38:17 +02:00
parent ba4b10ab71
commit 8930284cd7
13 changed files with 180 additions and 113 deletions

1
Cargo.lock generated
View file

@ -181,6 +181,7 @@ dependencies = [
"url",
"uuid",
"zbar-rust",
"zeroize",
]
[[package]]

View file

@ -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"]}

View file

@ -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>,
}

View file

@ -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(),

View file

@ -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>,
}

View file

@ -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,
}

View file

@ -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,
}
}

View file

@ -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,
}

View file

@ -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 {

View file

@ -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,
};

View file

@ -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]

View file

@ -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
View 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)
}
}