Fix OTP generation

This commit is contained in:
Maximiliano Sandoval R 2021-01-22 14:46:28 +01:00
parent 2630089c47
commit 59c748f61f
Failed to generate hash of commit
5 changed files with 85 additions and 29 deletions

2
Cargo.lock generated
View file

@ -247,7 +247,7 @@ dependencies = [
"anyhow",
"ashpd",
"async-std",
"byteorder",
"data-encoding",
"diesel",
"diesel_migrations",
"futures",

View file

@ -6,6 +6,7 @@ version = "0.1.0"
[dependencies]
anyhow = "1.0"
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", features=["feature_gtk4"] }
data-encoding = "2.1"
diesel = {version = "1.4", features = ["sqlite", "r2d2"]}
diesel_migrations = {version = "1.4", features = ["sqlite"]}
futures = "0.3"
@ -24,7 +25,6 @@ once_cell = "1.5"
unicase = "2.6"
sha2 = "0.9"
hex = "0.4"
byteorder = "1.3"
ring = "0.16"
serde = "1.0"
serde_json = "1.0"

View file

@ -2,10 +2,10 @@ use super::{
provider::{DiProvider, Provider},
OTPMethod, OTPUri,
};
use crate::models::otp;
use crate::widgets::QRCodeData;
use crate::{helpers::Keyring, models::database, schema::accounts};
use anyhow::Result;
use byteorder::{BigEndian, ReadBytesExt};
use core::cmp::Ordering;
use diesel::{BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use glib::{clone, Cast, ObjectExt, StaticType, ToValue};
@ -282,17 +282,18 @@ impl Account {
OTPMethod::Steam => 1,
};
let otp = generate_otp(
if let Ok(otp) = otp::generate_hotp(
&self.token(),
counter,
provider.algorithm().into(),
provider.digits() as u32,
);
self.set_property(
"otp",
&format!("{:0width$}", otp, width = provider.digits() as usize),
)
.unwrap();
) {
self.set_property(
"otp",
&format!("{:0width$}", otp, width = provider.digits() as usize),
)
.unwrap();
};
}
fn increment_counter(&self) -> Result<()> {
@ -384,22 +385,3 @@ impl Account {
Ok(())
}
}
pub(crate) fn generate_otp(
token: &str,
counter: u64,
algorithm: hmac::Algorithm,
digits: u32,
) -> u32 {
// Modified version of an implementation from otpauth crate
let key = hmac::Key::new(algorithm, token.as_bytes());
let wtr = counter.to_be_bytes().to_vec();
let result = hmac::sign(&key, &wtr);
let digest = result.as_ref();
let ob = digest[19];
let pos = (ob & 15) as usize;
let mut rdr = std::io::Cursor::new(digest[pos..pos + 4].to_vec());
let base = rdr.read_u32::<BigEndian>().unwrap() & 0x7fff_ffff;
base % 10_u32.pow(digits)
}

View file

@ -6,6 +6,7 @@ mod accounts;
mod algorithm;
pub mod database;
mod favicon;
pub mod otp;
mod otp_uri;
mod provider;
mod provider_sorter;

73
src/models/otp.rs Normal file
View file

@ -0,0 +1,73 @@
use anyhow::Result;
use data_encoding::{DecodeError, BASE32_NOPAD};
use ring::hmac;
use std::convert::TryInto;
use std::time::{SystemTime, SystemTimeError};
/// Code graciously taken from the rust-top crate.
/// https://github.com/TimDumol/rust-otp/blob/master/src/lib.rs
/// Decodes a secret (given as an RFC4648 base32-encoded ASCII string)
/// into a byte string
fn decode_secret(secret: &str) -> Result<Vec<u8>> {
let res = BASE32_NOPAD.decode(secret.as_bytes())?;
Ok(res)
}
/// Calculates the HMAC digest for the given secret and counter.
fn calc_digest(decoded_secret: &[u8], counter: u64, algorithm: hmac::Algorithm) -> hmac::Tag {
let key = hmac::Key::new(algorithm, decoded_secret);
hmac::sign(&key, &counter.to_be_bytes())
}
/// Encodes the HMAC digest into a n-digit integer.
fn encode_digest(digest: &[u8], digits: u32) -> Result<u32> {
let offset = match digest.last() {
Some(x) => *x & 0xf,
None => anyhow::bail!("Invalid digest"),
} as usize;
let code_bytes: [u8; 4] = match digest[offset..offset + 4].try_into() {
Ok(x) => x,
Err(_) => anyhow::bail!("Invalid digest"),
};
let code = u32::from_be_bytes(code_bytes);
Ok((code & 0x7fffffff) % 10_u32.pow(digits))
}
/// 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 generate_hotp(
secret: &str,
counter: u64,
algorithm: hmac::Algorithm,
digits: u32,
) -> Result<u32> {
let decoded = decode_secret(secret)?;
encode_digest(
calc_digest(decoded.as_slice(), counter, algorithm).as_ref(),
digits,
)
}
#[cfg(test)]
mod tests {
use super::make_hotp;
#[test]
fn hotp() {
let algorithm = hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY;
let digits: u32 = 6;
assert_eq!(
make_hotp("BASE32SECRET3232", 0, algorithm, digits).unwrap(),
260182
);
assert_eq!(
make_hotp("BASE32SECRET3232", 1, algorithm, digits).unwrap(),
55283
);
assert_eq!(
make_hotp("BASE32SECRET3232", 1401, algorithm, digits).unwrap(),
316439
);
}
}