otp: implement steam algorithm

fixes #115
This commit is contained in:
Bilal Elmoussaoui 2021-01-23 02:53:53 +01:00
parent e40843c102
commit 733af58e89
14 changed files with 57 additions and 83 deletions

View File

@ -0,0 +1 @@
DELETE FROM "providers" WHERE "name"="Steam";

View File

@ -0,0 +1 @@
INSERT INTO "providers" ("name", "website", "help_url", "digits", "method") VALUES ("Steam", "https://steamcommunity.com/", "https://steamcommunity.com/favicon.ico", 5, "steam");

View File

@ -1,7 +1,6 @@
use crate::{ use crate::{
config, config,
helpers::Keyring, models::{Keyring, ProvidersModel},
models::ProvidersModel,
widgets::{PreferencesWindow, ProvidersDialog, Window}, widgets::{PreferencesWindow, ProvidersDialog, Window},
}; };
use gettextrs::gettext; use gettextrs::gettext;

View File

@ -1,4 +0,0 @@
mod keyring;
pub mod qrcode;
pub use keyring::Keyring;

View File

@ -1,50 +0,0 @@
use crate::models::OTPUri;
use anyhow::Result;
use ashpd::{
desktop::screenshot::{Screenshot, ScreenshotOptions, ScreenshotProxy},
zbus, RequestProxy, Response, WindowIdentifier,
};
use gtk::{gio, prelude::*};
use image::GenericImageView;
use std::str::FromStr;
use zbar_rust::ZBarImageScanner;
pub(crate) fn scan(screenshot: &gio::File) -> Result<OTPUri> {
let (data, _) = screenshot.load_contents(gio::NONE_CANCELLABLE)?;
let img = image::load_from_memory(&data)?;
let (width, height) = img.dimensions();
let img_data: Vec<u8> = img.to_luma8().to_vec();
let mut scanner = ZBarImageScanner::new();
let results = scanner
.scan_y800(&img_data, width, height)
.map_err(|e| anyhow::format_err!(e))?;
if let Some(ref result) = results.get(0) {
let uri = String::from_utf8(result.data.clone())?;
return Ok(OTPUri::from_str(&uri)?);
}
anyhow::bail!("Invalid QR code")
}
pub(crate) fn screenshot_area<F: FnOnce(gio::File)>(
window: gtk::Window,
callback: F,
) -> Result<()> {
let connection = zbus::Connection::new_session()?;
let proxy = ScreenshotProxy::new(&connection)?;
let handle = proxy.screenshot(
WindowIdentifier::from(window),
ScreenshotOptions::default().interactive(true).modal(true),
)?;
let request = RequestProxy::new(&connection, &handle)?;
request.on_response(move |response: Response<Screenshot>| {
if let Ok(screenshot) = response {
callback(gio::File::new_for_uri(&screenshot.uri));
}
})?;
Ok(())
}

View File

@ -9,7 +9,6 @@ use gettextrs::*;
mod application; mod application;
mod backup; mod backup;
mod config; mod config;
mod helpers;
mod models; mod models;
mod schema; mod schema;
mod static_resources; mod static_resources;

View File

@ -42,17 +42,15 @@ sources = files(
'backup/freeotp.rs', 'backup/freeotp.rs',
'backup/legacy.rs', 'backup/legacy.rs',
'backup/mod.rs', 'backup/mod.rs',
'helpers/database.rs',
'helpers/keyring.rs',
'helpers/mod.rs',
'helpers/qrcode.rs',
'models/account_sorter.rs', 'models/account_sorter.rs',
'models/account.rs', 'models/account.rs',
'models/accounts.rs', 'models/accounts.rs',
'models/algorithm.rs', 'models/algorithm.rs',
'models/database.rs', 'models/database.rs',
'models/favicon.rs', 'models/favicon.rs',
'models/keyring.rs',
'models/mod.rs', 'models/mod.rs',
'models/otp.rs',
'models/otp_uri.rs', 'models/otp_uri.rs',
'models/provider_sorter.rs', 'models/provider_sorter.rs',
'models/provider.rs', 'models/provider.rs',

View File

@ -3,8 +3,7 @@ use super::{
OTPMethod, OTPUri, OTPMethod, OTPUri,
}; };
use crate::{ use crate::{
helpers::Keyring, models::{database, otp, Keyring},
models::{database, otp},
schema::accounts, schema::accounts,
widgets::QRCodeData, widgets::QRCodeData,
}; };
@ -268,7 +267,7 @@ impl Account {
let provider = self.provider(); let provider = self.provider();
let counter = match provider.method() { let counter = match provider.method() {
OTPMethod::TOTP => { OTPMethod::TOTP | OTPMethod::Steam => {
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
@ -282,21 +281,30 @@ impl Account {
} }
old_counter as u64 old_counter as u64
} }
OTPMethod::Steam => 1,
}; };
let label = match otp::hotp( let otp_password: Result<String> = match provider.method() {
&self.token(), OTPMethod::Steam => otp::steam(&self.token()),
counter, _ => {
provider.algorithm().into(), let token = otp::hotp(
provider.digits() as u32, &self.token(),
) { counter,
Ok(otp) => otp::format(otp, provider.digits() as usize), provider.algorithm().into(),
provider.digits() as u32,
);
token.map(|d| otp::format(d, provider.digits() as usize))
}
};
let label = match otp_password {
Ok(password) => password,
Err(err) => { Err(err) => {
debug!("Could not generate HOTP {:?}", err); warn!("Failed to generate the OTP {}", err);
"Error".to_string() "Error".to_string()
} }
}; };
self.set_property("otp", &label).unwrap(); self.set_property("otp", &label).unwrap();
} }

View File

@ -1,11 +1,11 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
mod account; mod account;
mod account_sorter; mod account_sorter;
mod accounts; mod accounts;
mod algorithm; mod algorithm;
pub mod database; pub mod database;
mod favicon; mod favicon;
mod keyring;
pub mod otp; pub mod otp;
mod otp_uri; mod otp_uri;
mod provider; mod provider;
@ -21,6 +21,7 @@ pub use self::{
accounts::AccountsModel, accounts::AccountsModel,
algorithm::{Algorithm, OTPMethod}, algorithm::{Algorithm, OTPMethod},
favicon::{FaviconError, FaviconScrapper}, favicon::{FaviconError, FaviconScrapper},
keyring::Keyring,
otp_uri::OTPUri, otp_uri::OTPUri,
provider::Provider, provider::Provider,
provider_sorter::ProviderSorter, provider_sorter::ProviderSorter,

View File

@ -1,8 +1,13 @@
use super::Algorithm;
use anyhow::Result; use anyhow::Result;
use data_encoding::BASE32_NOPAD; use data_encoding::BASE32_NOPAD;
use ring::hmac; use ring::hmac;
use std::convert::TryInto; use std::convert::TryInto;
static STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY";
static STEAM_DEFAULT_COUNTER: u64 = 30;
static STEAM_DEFAULT_DIGITS: u32 = 5;
/// Code graciously taken from the rust-top crate. /// Code graciously taken from the rust-top crate.
/// https://github.com/TimDumol/rust-otp/blob/master/src/lib.rs /// https://github.com/TimDumol/rust-otp/blob/master/src/lib.rs
@ -20,7 +25,7 @@ fn calc_digest(decoded_secret: &[u8], counter: u64, algorithm: hmac::Algorithm)
} }
/// Encodes the HMAC digest into a n-digit integer. /// Encodes the HMAC digest into a n-digit integer.
fn encode_digest(digest: &[u8], digits: u32) -> Result<u32> { fn encode_digest(digest: &[u8]) -> Result<u32> {
let offset = match digest.last() { let offset = match digest.last() {
Some(x) => *x & 0xf, Some(x) => *x & 0xf,
None => anyhow::bail!("Invalid digest"), None => anyhow::bail!("Invalid digest"),
@ -30,7 +35,7 @@ fn encode_digest(digest: &[u8], digits: u32) -> Result<u32> {
Err(_) => anyhow::bail!("Invalid digest"), Err(_) => anyhow::bail!("Invalid digest"),
}; };
let code = u32::from_be_bytes(code_bytes); let code = u32::from_be_bytes(code_bytes);
Ok((code & 0x7fffffff) % 10_u32.pow(digits)) Ok(code & 0x7fffffff)
} }
/// Performs the [HMAC-based One-time Password Algorithm](http://en.wikipedia.org/wiki/HMAC-based_One-time_Password_Algorithm) /// Performs the [HMAC-based One-time Password Algorithm](http://en.wikipedia.org/wiki/HMAC-based_One-time_Password_Algorithm)
@ -42,10 +47,27 @@ pub(crate) fn hotp(
digits: u32, digits: u32,
) -> Result<u32> { ) -> Result<u32> {
let decoded = decode_secret(secret)?; let decoded = decode_secret(secret)?;
encode_digest( let digest = encode_digest(calc_digest(decoded.as_slice(), counter, algorithm).as_ref())?;
calc_digest(decoded.as_slice(), counter, algorithm).as_ref(), Ok(digest % 10_u32.pow(digits))
digits, }
)
pub(crate) fn steam(secret: &str) -> Result<String> {
let mut token = hotp(
secret,
STEAM_DEFAULT_COUNTER,
Algorithm::SHA1.into(),
STEAM_DEFAULT_DIGITS,
)?;
let mut code = String::new();
let total_chars = STEAM_CHARS.len() as u32;
for i in 0..5 {
let pos = token % total_chars;
println!("{:#?}", pos);
let charachter = STEAM_CHARS.chars().nth(pos as usize).unwrap();
code.push(charachter);
token = token / total_chars;
}
Ok(code)
} }
pub(crate) fn format(code: u32, digits: usize) -> String { pub(crate) fn format(code: u32, digits: usize) -> String {

View File

@ -1,4 +1,4 @@
use crate::{config, helpers::Keyring}; use crate::{config, models::Keyring};
use glib::clone; use glib::clone;
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate}; use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk_macros::{action, get_action}; use gtk_macros::{action, get_action};

View File

@ -1,8 +1,7 @@
use crate::{ use crate::{
application::Application, application::Application,
config, config,
helpers::Keyring, models::{Account, Keyring, ProvidersModel},
models::{Account, ProvidersModel},
widgets::{accounts::AccountDetailsPage, providers::ProvidersList, AccountAddDialog}, widgets::{accounts::AccountDetailsPage, providers::ProvidersList, AccountAddDialog},
window_state, window_state,
}; };