mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
Switch to oo7 for secret service handling
Also switch to tracing while we are at it. Might need to refactor the whole keyring module in a later commit
This commit is contained in:
parent
68029e5e42
commit
3c3eb02fff
26 changed files with 547 additions and 590 deletions
633
Cargo.lock
generated
633
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -17,15 +17,15 @@ gtk = {package = "gtk4", version = "0.4"}
|
|||
gtk-macros = "0.3"
|
||||
search-provider = "0.3"
|
||||
hex = { version = "0.4.3", features = [ "serde" ] }
|
||||
log = "0.4"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
once_cell = "1.9"
|
||||
percent-encoding = "2.1"
|
||||
pretty_env_logger = "0.4"
|
||||
qrcode = {version = "0.12", features = ["image"]}
|
||||
rand = "0.8"
|
||||
ring = "0.16"
|
||||
rust-argon2 = "1.0"
|
||||
secret-service = "2.0"
|
||||
oo7 = {version = "0.1.0-alpha.3", default-features = false, features = ["tokio_runtime", "tracing"]}
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
unicase = "2.6"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
"--talk-name=org.freedesktop.secrets",
|
||||
"--env=RUST_LOG=authenticator=debug,ashpd=debug",
|
||||
"--env=RUST_LOG=authenticator=debug,ashpd=debug,oo7=debug",
|
||||
"--env=G_MESSAGES_DEBUG=none",
|
||||
"--own-name=com.belmoussaoui.Authenticator.Devel.SearchProvider"
|
||||
],
|
||||
|
|
|
@ -31,5 +31,8 @@
|
|||
<summary>Auto lock timeout</summary>
|
||||
<description>Lock the application on idle after X minutes</description>
|
||||
</key>
|
||||
<key name="keyrings-migrated" type="b">
|
||||
<default>false</default>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
|
|
|
@ -13,7 +13,7 @@ quick-xml = "0.22"
|
|||
image = {version = "0.24", features = ["ico", "png"], default-features = false}
|
||||
url = "2.2"
|
||||
once_cell = "1.9"
|
||||
log = "0.4"
|
||||
tracing = "0.1"
|
||||
base64 = "0.13.0"
|
||||
svg_metadata = "0.4"
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread", "fs", "io-util", "macros"] }
|
||||
|
|
|
@ -77,16 +77,16 @@ impl Favicon {
|
|||
/// Save the favicon into `destination` and convert it to a [`Format::Png`]
|
||||
/// if it is original format is [`Format::Ico`].
|
||||
pub async fn save(&self, destination: PathBuf) -> Result<(), Error> {
|
||||
log::debug!("Caching the icon into {:#?}", destination);
|
||||
tracing::debug!("Caching the icon into {:#?}", destination);
|
||||
let format = *self.metadata().format();
|
||||
let body = self.data().await?;
|
||||
if format.is_ico() {
|
||||
log::debug!("Found a ICO favicon, converting to PNG");
|
||||
tracing::debug!("Found a ICO favicon, converting to PNG");
|
||||
if let Ok(ico) = image::load_from_memory_with_format(&body, image::ImageFormat::Ico) {
|
||||
ico.save_with_format(destination, image::ImageFormat::Png)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
log::debug!("It seems to not be a ICO favicon, fallback to PNG");
|
||||
tracing::debug!("It seems to not be a ICO favicon, fallback to PNG");
|
||||
};
|
||||
}
|
||||
let mut dest = tokio::fs::File::create(destination).await?;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use log::debug;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use quick_xml::events::{attributes::Attribute, BytesStart, Event};
|
||||
use std::{fmt, path::PathBuf};
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
|
||||
use crate::{Error, Favicon, Format, Metadata, CLIENT};
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::{
|
||||
config,
|
||||
models::{Account, Keyring, OTPUri, Provider, ProvidersModel, FAVICONS_PATH},
|
||||
models::{
|
||||
keyring, Account, OTPUri, Provider, ProvidersModel, FAVICONS_PATH, RUNTIME, SECRET_SERVICE,
|
||||
},
|
||||
widgets::{PreferencesWindow, ProvidersDialog, Window},
|
||||
};
|
||||
use adw::prelude::*;
|
||||
|
@ -9,12 +11,15 @@ use glib::clone;
|
|||
use gtk::{gio, glib, subclass::prelude::*};
|
||||
use gtk_macros::{action, get_action};
|
||||
use search_provider::{ResultID, ResultMeta, SearchProvider, SearchProviderImpl};
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
mod imp {
|
||||
use crate::utils::spawn_tokio_blocking;
|
||||
|
||||
use super::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::{ParamSpec, ParamSpecBoolean, Value, WeakRef};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
// The basic struct that holds our state and widgets
|
||||
|
@ -25,7 +30,7 @@ mod imp {
|
|||
pub locked: Cell<bool>,
|
||||
pub lock_timeout_id: RefCell<Option<glib::SourceId>>,
|
||||
pub can_be_locked: Cell<bool>,
|
||||
pub settings: gio::Settings,
|
||||
pub settings: OnceCell<gio::Settings>,
|
||||
pub search_provider: RefCell<Option<SearchProvider<super::Application>>>,
|
||||
}
|
||||
|
||||
|
@ -39,12 +44,11 @@ mod imp {
|
|||
// Initialize with default values
|
||||
fn new() -> Self {
|
||||
let model = ProvidersModel::new();
|
||||
let settings = gio::Settings::new(config::APP_ID);
|
||||
|
||||
Self {
|
||||
window: RefCell::new(None),
|
||||
model,
|
||||
settings,
|
||||
settings: OnceCell::default(),
|
||||
can_be_locked: Cell::new(false),
|
||||
lock_timeout_id: RefCell::default(),
|
||||
locked: Cell::new(false),
|
||||
|
@ -188,7 +192,7 @@ mod imp {
|
|||
}
|
||||
});
|
||||
|
||||
self.settings.connect_changed(
|
||||
self.settings.get().unwrap().connect_changed(
|
||||
None,
|
||||
clone!(@weak app => move |settings, key| {
|
||||
match key {
|
||||
|
@ -217,7 +221,7 @@ mod imp {
|
|||
Ok(search_provider) => {
|
||||
imp.search_provider.replace(Some(search_provider));
|
||||
},
|
||||
Err(err) => log::debug!("Could not start search provider: {}", err),
|
||||
Err(err) => tracing::debug!("Could not start search provider: {}", err),
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
@ -233,7 +237,8 @@ mod imp {
|
|||
window.present();
|
||||
self.window.replace(Some(window.downgrade()));
|
||||
|
||||
let has_set_password = Keyring::has_set_password().unwrap_or(false);
|
||||
let has_set_password =
|
||||
spawn_tokio_blocking(async { keyring::has_set_password().await.unwrap_or(false) });
|
||||
app.set_accels_for_action("app.quit", &["<primary>q"]);
|
||||
app.set_accels_for_action("app.lock", &["<primary>l"]);
|
||||
app.set_accels_for_action("app.providers", &["<primary>p"]);
|
||||
|
@ -283,13 +288,50 @@ glib::wrapper! {
|
|||
|
||||
impl Application {
|
||||
pub fn run() {
|
||||
info!("Authenticator ({})", config::APP_ID);
|
||||
info!("Version: {} ({})", config::VERSION, config::PROFILE);
|
||||
info!("Datadir: {}", config::PKGDATADIR);
|
||||
tracing::info!("Authenticator ({})", config::APP_ID);
|
||||
tracing::info!("Version: {} ({})", config::VERSION, config::PROFILE);
|
||||
tracing::info!("Datadir: {}", config::PKGDATADIR);
|
||||
|
||||
std::fs::create_dir_all(&*FAVICONS_PATH.clone()).ok();
|
||||
Keyring::ensure_unlocked()
|
||||
.expect("Authenticator couldn't reach a secret service provider or unlock it");
|
||||
|
||||
// To be removed in the upcoming release
|
||||
let settings = gio::Settings::new(config::APP_ID);
|
||||
if !settings.boolean("keyrings-migrated") {
|
||||
tracing::info!("Migrating the secrets to the file backend");
|
||||
let output: oo7::Result<()> = RUNTIME.block_on(async {
|
||||
oo7::migrate(
|
||||
vec![
|
||||
HashMap::from([("application", config::APP_ID), ("type", "token")]),
|
||||
HashMap::from([("application", config::APP_ID), ("type", "password")]),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
});
|
||||
match output {
|
||||
Ok(_) => {
|
||||
settings
|
||||
.set_boolean("keyrings-migrated", true)
|
||||
.expect("Failed to update settings");
|
||||
tracing::info!("Secrets were migrated successfully");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to migrate your data {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
let keyring = oo7::Keyring::new()
|
||||
.await
|
||||
.expect("Failed to start a location service");
|
||||
keyring
|
||||
.unlock()
|
||||
.await
|
||||
.expect("Failed to unlock the default collection");
|
||||
SECRET_SERVICE.set(keyring).unwrap()
|
||||
});
|
||||
|
||||
let app = glib::Object::new::<Application>(&[
|
||||
("application-id", &Some(config::APP_ID)),
|
||||
|
@ -297,6 +339,8 @@ impl Application {
|
|||
("resource-base-path", &"/com/belmoussaoui/Authenticator"),
|
||||
])
|
||||
.unwrap();
|
||||
let imp = app.imp();
|
||||
imp.settings.set(settings).unwrap();
|
||||
|
||||
ApplicationExtManual::run(&app);
|
||||
}
|
||||
|
@ -334,8 +378,8 @@ impl Application {
|
|||
/// Starts or restarts the lock timeout.
|
||||
pub fn restart_lock_timeout(&self) {
|
||||
let imp = self.imp();
|
||||
let auto_lock = imp.settings.boolean("auto-lock");
|
||||
let timeout = imp.settings.uint("auto-lock-timeout") * 60;
|
||||
let auto_lock = imp.settings.get().unwrap().boolean("auto-lock");
|
||||
let timeout = imp.settings.get().unwrap().uint("auto-lock-timeout") * 60;
|
||||
|
||||
if !auto_lock {
|
||||
return;
|
||||
|
@ -364,7 +408,7 @@ impl Application {
|
|||
fn update_color_scheme(&self) {
|
||||
let manager = self.style_manager();
|
||||
if !manager.system_supports_color_schemes() {
|
||||
let color_scheme = if self.imp().settings.boolean("dark-theme") {
|
||||
let color_scheme = if self.imp().settings.get().unwrap().boolean("dark-theme") {
|
||||
adw::ColorScheme::PreferDark
|
||||
} else {
|
||||
adw::ColorScheme::PreferLight
|
||||
|
|
|
@ -157,7 +157,7 @@ impl Aegis {
|
|||
// Check whether file is encrypted or in plaintext
|
||||
match aegis_root {
|
||||
Aegis::Plaintext(plain_text) => {
|
||||
log::info!(
|
||||
tracing::info!(
|
||||
"Found unencrypted aegis vault with version {} and database version {}.",
|
||||
plain_text.version,
|
||||
plain_text.db.version
|
||||
|
@ -180,7 +180,7 @@ impl Aegis {
|
|||
}
|
||||
}
|
||||
Aegis::Encrypted(encrypted) => {
|
||||
log::info!(
|
||||
tracing::info!(
|
||||
"Found encrypted aegis vault with version {}.",
|
||||
encrypted.version
|
||||
);
|
||||
|
@ -212,7 +212,7 @@ impl Aegis {
|
|||
.iter()
|
||||
.filter(|slot| slot.type_ == 1) // We don't handle biometric slots for now
|
||||
.map(|slot| -> Result<Vec<u8>> {
|
||||
log::info!("Found possible master key with UUID {}.", slot.uuid);
|
||||
tracing::info!("Found possible master key with UUID {}.", slot.uuid);
|
||||
|
||||
// Create parameters for scrypt function and derive decryption key for master key
|
||||
//
|
||||
|
@ -250,20 +250,20 @@ impl Aegis {
|
|||
.filter_map(|x| match x {
|
||||
Ok(x) => Some(x),
|
||||
Err(e) => {
|
||||
log::error!("Decrypting master key failed: {:?}", e);
|
||||
tracing::error!("Decrypting master key failed: {:?}", e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Choose the first valid master key. I don't think there are aegis installations with two valid password slots.
|
||||
log::info!(
|
||||
tracing::info!(
|
||||
"Found {} valid password slots / master keys.",
|
||||
master_keys.len()
|
||||
);
|
||||
let master_key = match master_keys.first() {
|
||||
Some(x) => {
|
||||
log::info!("Using only the first valid key slot / master key.");
|
||||
tracing::info!("Using only the first valid key slot / master key.");
|
||||
x
|
||||
}
|
||||
None => anyhow::bail!(
|
||||
|
@ -288,7 +288,7 @@ impl Aegis {
|
|||
.context("Deserialize decrypted database failed")?;
|
||||
|
||||
// Check version of the database
|
||||
log::info!("Found aegis database with version {}.", db.version);
|
||||
tracing::info!("Found aegis database with version {}.", db.version);
|
||||
if encrypted.version > 2 {
|
||||
anyhow::bail!(
|
||||
"Aegis database version expected to be 1 or 2. Found {} instead.",
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
use gtk::{gio, glib};
|
||||
|
||||
mod utils;
|
||||
use gettextrs::*;
|
||||
mod application;
|
||||
mod backup;
|
||||
|
@ -25,13 +24,13 @@ fn init_i18n() -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
tracing_subscriber::fmt::init();
|
||||
gtk::init().expect("failed to init gtk");
|
||||
gst::init().expect("failed to init gstreamer");
|
||||
gst4gtk::plugin_register_static().expect("Failed to register gstgtk4 plugin");
|
||||
|
||||
if let Err(err) = init_i18n() {
|
||||
error!("Failed to initialize i18n {}", err);
|
||||
tracing::error!("Failed to initialize i18n {}", err);
|
||||
}
|
||||
|
||||
let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/authenticator.gresource")
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use super::{
|
||||
provider::{DiProvider, Provider},
|
||||
OTPMethod, OTPUri,
|
||||
OTPMethod, OTPUri, RUNTIME,
|
||||
};
|
||||
use crate::{
|
||||
models::{database, otp, Keyring},
|
||||
models::{database, keyring, otp},
|
||||
schema::accounts,
|
||||
utils::spawn_tokio_blocking,
|
||||
widgets::QRCodeData,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
|
@ -177,8 +178,13 @@ impl Account {
|
|||
let db = database::connection();
|
||||
let conn = db.get()?;
|
||||
|
||||
let token_id = Keyring::store(&format!("{} - {}", provider.name(), name), token)
|
||||
.context("Failed to save token")?;
|
||||
let label = format!("{} - {}", provider.name(), name);
|
||||
let token_send = token.to_owned();
|
||||
let token_id = spawn_tokio_blocking(async move {
|
||||
keyring::store(&label, &token_send)
|
||||
.await
|
||||
.context("Failed to save token")
|
||||
})?;
|
||||
|
||||
diesel::insert_into(accounts::table)
|
||||
.values(NewAccount {
|
||||
|
@ -255,7 +261,12 @@ impl Account {
|
|||
let token = if let Some(t) = token {
|
||||
t.to_string()
|
||||
} else {
|
||||
Keyring::token(token_id)?.context("Could not get item from keyring")?
|
||||
let token_id = token_id.to_owned();
|
||||
spawn_tokio_blocking(async move {
|
||||
keyring::token(&token_id)
|
||||
.await?
|
||||
.context("Could not get item from keyring")
|
||||
})?
|
||||
};
|
||||
account.imp().token.set(token).unwrap();
|
||||
account.generate_otp();
|
||||
|
@ -288,7 +299,7 @@ impl Account {
|
|||
let label = match otp_password {
|
||||
Ok(password) => password,
|
||||
Err(err) => {
|
||||
warn!("Failed to generate the OTP {}", err);
|
||||
tracing::warn!("Failed to generate the OTP {}", err);
|
||||
"Error".to_string()
|
||||
}
|
||||
};
|
||||
|
@ -402,9 +413,9 @@ impl Account {
|
|||
|
||||
pub fn delete(&self) -> Result<()> {
|
||||
let token_id = self.token_id();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = Keyring::remove_token(&token_id) {
|
||||
error!("Failed to remove the token from secret service {}", err);
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(err) = keyring::remove_token(&token_id).await {
|
||||
tracing::error!("Failed to remove the token from secret service {}", err);
|
||||
}
|
||||
});
|
||||
let db = database::connection();
|
||||
|
|
|
@ -15,7 +15,7 @@ pub(crate) fn connection() -> Pool {
|
|||
}
|
||||
|
||||
fn run_migration_on(connection: &SqliteConnection) -> Result<()> {
|
||||
info!("Running DB Migrations...");
|
||||
tracing::info!("Running DB Migrations...");
|
||||
embedded_migrations::run_with_output(connection, &mut std::io::stdout()).map_err(From::from)
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,6 @@ fn init_pool() -> Result<Pool> {
|
|||
let db = pool.get()?;
|
||||
run_migration_on(&*db)?;
|
||||
}
|
||||
info!("Database pool initialized.");
|
||||
tracing::info!("Database pool initialized.");
|
||||
Ok(pool)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
use crate::config;
|
||||
use once_cell::sync::Lazy;
|
||||
use secret_service::{Collection, EncryptionType, Error, SecretService};
|
||||
use once_cell::sync::OnceCell;
|
||||
use rand::RngCore;
|
||||
use std::collections::HashMap;
|
||||
|
||||
static SECRET_SERVICE: Lazy<SecretService<'static>> = Lazy::new(|| {
|
||||
SecretService::new(EncryptionType::Dh)
|
||||
.expect("A running secret-service is required for Authenticator")
|
||||
});
|
||||
|
||||
pub struct Keyring;
|
||||
pub static SECRET_SERVICE: OnceCell<oo7::Keyring> = OnceCell::new();
|
||||
|
||||
fn token_attributes(token_id: &str) -> HashMap<&str, &str> {
|
||||
let mut attributes = HashMap::new();
|
||||
|
@ -25,135 +20,94 @@ fn password_attributes() -> HashMap<&'static str, &'static str> {
|
|||
attributes
|
||||
}
|
||||
|
||||
fn random_salt() -> [u8; 64] {
|
||||
use rand::RngCore;
|
||||
|
||||
fn encode_argon2(secret: &str) -> anyhow::Result<String> {
|
||||
let password = secret.as_bytes();
|
||||
let mut salt = [0u8; 64];
|
||||
rand::thread_rng().fill_bytes(&mut salt);
|
||||
salt
|
||||
}
|
||||
|
||||
fn encode_argon2(secret: &str) -> anyhow::Result<String> {
|
||||
use argon2::Config;
|
||||
|
||||
let password = secret.as_bytes();
|
||||
let salt = &random_salt();
|
||||
let config = Config::default();
|
||||
let hash = argon2::hash_encoded(password, salt, &config)?;
|
||||
let config = argon2::Config::default();
|
||||
let hash = argon2::hash_encoded(password, &salt, &config)?;
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// Verifies that the hash generated by `password` corresponds
|
||||
/// to `hash`.
|
||||
fn verify_argon2(hash: &str, password: &str) -> anyhow::Result<bool> {
|
||||
Ok(argon2::verify_encoded(hash, password.as_bytes())?)
|
||||
pub async fn store(label: &str, token: &str) -> anyhow::Result<String> {
|
||||
let token_id = encode_argon2(token)?;
|
||||
let attributes = token_attributes(&token_id);
|
||||
let base64_encoded_token = hex::encode(token.as_bytes());
|
||||
SECRET_SERVICE
|
||||
.get()
|
||||
.unwrap()
|
||||
.create_item(label, attributes, base64_encoded_token.as_bytes(), true)
|
||||
.await?;
|
||||
Ok(token_id)
|
||||
}
|
||||
|
||||
impl Keyring {
|
||||
pub fn get_default_collection<'a>() -> Result<Collection<'a>, Error> {
|
||||
let collection = match SECRET_SERVICE.get_default_collection() {
|
||||
Err(Error::NoResult) => SECRET_SERVICE.create_collection("default", "default"),
|
||||
e => e,
|
||||
}?;
|
||||
pub async fn token(token_id: &str) -> anyhow::Result<Option<String>> {
|
||||
let attributes = token_attributes(token_id);
|
||||
let items = SECRET_SERVICE
|
||||
.get()
|
||||
.unwrap()
|
||||
.search_items(attributes)
|
||||
.await?;
|
||||
Ok(match items.get(0) {
|
||||
Some(e) => Some(String::from_utf8(hex::decode(&*e.secret().await?).unwrap()).unwrap()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(collection)
|
||||
pub async fn remove_token(token_id: &str) -> anyhow::Result<()> {
|
||||
let attributes = token_attributes(token_id);
|
||||
SECRET_SERVICE.get().unwrap().delete(attributes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn has_set_password() -> anyhow::Result<bool> {
|
||||
let attributes = password_attributes();
|
||||
match SECRET_SERVICE.get().unwrap().search_items(attributes).await {
|
||||
Ok(items) => Ok(matches!(items.get(0), Some(_))),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_unlocked() -> Result<(), Error> {
|
||||
let collection = Self::get_default_collection()?;
|
||||
collection.unlock()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store(label: &str, token: &str) -> anyhow::Result<String> {
|
||||
let col = Self::get_default_collection()?;
|
||||
let token_id = encode_argon2(token)?;
|
||||
let attributes = token_attributes(&token_id);
|
||||
let base64_encoded_token = hex::encode(token.as_bytes());
|
||||
col.create_item(
|
||||
label,
|
||||
attributes,
|
||||
base64_encoded_token.as_bytes(),
|
||||
true,
|
||||
"text/plain",
|
||||
)?;
|
||||
Ok(token_id)
|
||||
}
|
||||
|
||||
pub fn token(token_id: &str) -> Result<Option<String>, Error> {
|
||||
let col = Self::get_default_collection()?;
|
||||
|
||||
let attributes = token_attributes(token_id);
|
||||
let items = col.search_items(attributes)?;
|
||||
Ok(match items.get(0) {
|
||||
Some(e) => Some(String::from_utf8(hex::decode(e.get_secret()?).unwrap()).unwrap()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_token(token_id: &str) -> Result<(), Error> {
|
||||
let col = Self::get_default_collection()?;
|
||||
|
||||
let attributes = token_attributes(token_id);
|
||||
let items = col.search_items(attributes)?;
|
||||
match items.get(0) {
|
||||
Some(e) => e.delete(),
|
||||
_ => Err(Error::NoResult),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_set_password() -> Result<bool, Error> {
|
||||
let col = Self::get_default_collection()?;
|
||||
|
||||
let attributes = password_attributes();
|
||||
match col.search_items(attributes) {
|
||||
Ok(items) => Ok(matches!(items.get(0), Some(_))),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores password using the Argon2 algorithm with a random 128bit salt.
|
||||
pub fn set_password(password: &str) -> anyhow::Result<()> {
|
||||
let col = Self::get_default_collection()?;
|
||||
|
||||
let encoded_password = encode_argon2(password)?;
|
||||
let attributes = password_attributes();
|
||||
col.create_item(
|
||||
/// Stores password using the Argon2 algorithm with a random 128bit salt.
|
||||
pub async fn set_password(password: &str) -> anyhow::Result<()> {
|
||||
let encoded_password = encode_argon2(password)?;
|
||||
let attributes = password_attributes();
|
||||
SECRET_SERVICE
|
||||
.get()
|
||||
.unwrap()
|
||||
.create_item(
|
||||
"Authenticator password",
|
||||
attributes,
|
||||
encoded_password.as_bytes(),
|
||||
true,
|
||||
"plain",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset_password() -> Result<(), Error> {
|
||||
let col = Self::get_default_collection()?;
|
||||
|
||||
let attributes = password_attributes();
|
||||
let items = col.search_items(attributes)?;
|
||||
|
||||
match items.get(0) {
|
||||
Some(i) => i.delete(),
|
||||
None => Err(Error::NoResult),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_current_password(password: &str) -> anyhow::Result<bool> {
|
||||
let col = Self::get_default_collection()?;
|
||||
|
||||
let attributes = password_attributes();
|
||||
let items = col.search_items(attributes)?;
|
||||
Ok(match items.get(0) {
|
||||
Some(i) => {
|
||||
let secret = &i.get_secret()?;
|
||||
let stored_pass = std::str::from_utf8(secret)?;
|
||||
verify_argon2(stored_pass, password)?
|
||||
}
|
||||
None => false,
|
||||
})
|
||||
}
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_password() -> anyhow::Result<()> {
|
||||
let attributes = password_attributes();
|
||||
SECRET_SERVICE.get().unwrap().delete(attributes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_current_password(password: &str) -> anyhow::Result<bool> {
|
||||
let attributes = password_attributes();
|
||||
let items = SECRET_SERVICE
|
||||
.get()
|
||||
.unwrap()
|
||||
.search_items(attributes)
|
||||
.await?;
|
||||
Ok(match items.get(0) {
|
||||
Some(i) => {
|
||||
// Verifies that the hash generated by `password` corresponds
|
||||
// to `hash`.
|
||||
argon2::verify_encoded(
|
||||
&String::from_utf8_lossy(&*i.secret().await?),
|
||||
password.as_bytes(),
|
||||
)?
|
||||
}
|
||||
None => false,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ mod accounts;
|
|||
mod algorithm;
|
||||
pub mod database;
|
||||
pub mod i18n;
|
||||
mod keyring;
|
||||
pub mod keyring;
|
||||
pub mod otp;
|
||||
mod otp_uri;
|
||||
mod provider;
|
||||
|
@ -25,7 +25,7 @@ pub use self::{
|
|||
account_sorter::AccountSorter,
|
||||
accounts::AccountsModel,
|
||||
algorithm::{Algorithm, OTPMethod},
|
||||
keyring::Keyring,
|
||||
keyring::SECRET_SERVICE,
|
||||
otp_uri::OTPUri,
|
||||
provider::{Provider, ProviderPatch},
|
||||
provider_sorter::ProviderSorter,
|
||||
|
|
|
@ -381,7 +381,7 @@ impl Provider {
|
|||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let website_url = Url::parse(&website)?;
|
||||
let favicon = favicon_scrapper::Scrapper::from_url(website_url).await?;
|
||||
log::debug!("Found the following icons {:#?} for {}", favicon, name);
|
||||
tracing::debug!("Found the following icons {:#?} for {}", favicon, name);
|
||||
|
||||
let icon_name = format!("{}_{}", id, name.replace(' ', "_"));
|
||||
let icon_name = glib::base64_encode(icon_name.as_bytes());
|
||||
|
@ -392,15 +392,15 @@ impl Provider {
|
|||
// - 32x32 for the accounts lists
|
||||
// - 96x96 elsewhere
|
||||
if let Some(best_favicon) = favicon.find_best().await {
|
||||
log::debug!("Largest favicon found is {:#?}", best_favicon);
|
||||
tracing::debug!("Largest favicon found is {:#?}", best_favicon);
|
||||
let cache_path = FAVICONS_PATH.join(&*icon_name);
|
||||
best_favicon.save(cache_path.clone()).await?;
|
||||
// Don't try to scale down svg variants
|
||||
if !best_favicon.metadata().format().is_svg() {
|
||||
log::debug!("Creating scaled down variants for {:#?}", cache_path);
|
||||
tracing::debug!("Creating scaled down variants for {:#?}", cache_path);
|
||||
{
|
||||
let pixbuf = gdk_pixbuf::Pixbuf::from_file(cache_path.clone())?;
|
||||
log::debug!("Creating a 32x32 variant of the favicon");
|
||||
tracing::debug!("Creating a 32x32 variant of the favicon");
|
||||
let small_pixbuf = pixbuf
|
||||
.scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
|
||||
.unwrap();
|
||||
|
@ -409,7 +409,7 @@ impl Provider {
|
|||
small_cache.set_file_name(small_icon_name);
|
||||
small_pixbuf.savev(small_cache.clone(), "png", &[])?;
|
||||
|
||||
log::debug!("Creating a 96x96 variant of the favicon");
|
||||
tracing::debug!("Creating a 96x96 variant of the favicon");
|
||||
let large_pixbuf = pixbuf
|
||||
.scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
|
||||
.unwrap();
|
||||
|
|
29
src/utils.rs
Normal file
29
src/utils.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use crate::models::RUNTIME;
|
||||
|
||||
pub fn spawn_tokio_blocking<F>(fut: F) -> F::Output
|
||||
where
|
||||
F: std::future::Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||
|
||||
RUNTIME.spawn(async {
|
||||
let response = fut.await;
|
||||
sender.send(response)
|
||||
});
|
||||
receiver.blocking_recv().unwrap()
|
||||
}
|
||||
|
||||
pub async fn spawn_tokio<F>(fut: F) -> F::Output
|
||||
where
|
||||
F: std::future::Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||
|
||||
RUNTIME.spawn(async {
|
||||
let response = fut.await;
|
||||
sender.send(response)
|
||||
});
|
||||
receiver.await.unwrap()
|
||||
}
|
|
@ -154,7 +154,7 @@ impl AccountAddDialog {
|
|||
fn scan_from_screenshot(&self) {
|
||||
spawn!(clone!(@weak self as page => async move {
|
||||
if let Err(err) = page.imp().camera.from_screenshot().await {
|
||||
log::error!("Failed to scan from screenshot {}", err);
|
||||
tracing::error!("Failed to scan from screenshot {}", err);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ mod imp {
|
|||
});
|
||||
klass.install_action("account.save", None, move |page, _, _| {
|
||||
if let Err(err) = page.save() {
|
||||
log::error!("Failed to save account details {}", err);
|
||||
tracing::error!("Failed to save account details {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ mod imp {
|
|||
klass.install_action("account.increment-counter", None, move |row, _, _| {
|
||||
match row.account().increment_counter() {
|
||||
Ok(_) => row.account().generate_otp(),
|
||||
Err(err) => log::error!("Failed to increment the counter {err}"),
|
||||
Err(err) => tracing::error!("Failed to increment the counter {err}"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ impl Camera {
|
|||
|
||||
fn set_state(&self, state: CameraState) {
|
||||
let imp = self.imp();
|
||||
info!("The camera state changed to {:#?}", state);
|
||||
tracing::info!("The camera state changed to {:#?}", state);
|
||||
match state {
|
||||
CameraState::NotFound => {
|
||||
imp.stack.set_visible_child_name("not-found");
|
||||
|
@ -199,13 +199,13 @@ impl Camera {
|
|||
Ok(Some((stream_fd, node_id))) => {
|
||||
match camera.imp().paintable.set_pipewire_node_id(stream_fd, node_id) {
|
||||
Ok(_) => camera.start(),
|
||||
Err(err) => log::error!("Failed to start the camera stream {err}"),
|
||||
Err(err) => tracing::error!("Failed to start the camera stream {err}"),
|
||||
};
|
||||
},
|
||||
Ok(None) => {
|
||||
camera.set_state(CameraState::NotFound);
|
||||
}
|
||||
Err(e) => log::error!("Failed to stream {}", e),
|
||||
Err(e) => tracing::error!("Failed to stream {}", e),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use gtk::{
|
|||
};
|
||||
use gtk_macros::send;
|
||||
use once_cell::sync::Lazy;
|
||||
use tracing::error;
|
||||
static PIPELINE_NAME: Lazy<glib::GString> = Lazy::new(|| glib::GString::from("camera"));
|
||||
/// Fancy Camera with QR code detection using ZBar
|
||||
///
|
||||
|
@ -125,16 +126,16 @@ impl CameraPaintable {
|
|||
pipewire_element.set_property("fd", &raw_fd);
|
||||
if let Some(node_id) = node_id {
|
||||
pipewire_element.set_property("path", &node_id.to_string());
|
||||
log::debug!("Loading PipeWire Node ID: {} with FD: {}", node_id, raw_fd);
|
||||
tracing::debug!("Loading PipeWire Node ID: {} with FD: {}", node_id, raw_fd);
|
||||
} else {
|
||||
log::debug!("Loading PipeWire with FD: {}", raw_fd);
|
||||
tracing::debug!("Loading PipeWire with FD: {}", raw_fd);
|
||||
}
|
||||
self.init_pipeline(pipewire_element)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_pipeline(&self, pipewire_src: gst::Element) -> anyhow::Result<()> {
|
||||
log::debug!("Init pipeline");
|
||||
tracing::debug!("Init pipeline");
|
||||
let imp = self.imp();
|
||||
let pipeline = gst::Pipeline::new(None);
|
||||
|
||||
|
@ -187,7 +188,7 @@ impl CameraPaintable {
|
|||
let sender = paintable.imp().sender.borrow().as_ref().unwrap().clone();
|
||||
match msg.view() {
|
||||
MessageView::Error(err) => {
|
||||
log::error!(
|
||||
tracing::error!(
|
||||
"Error from {:?}: {} ({:?})",
|
||||
err.src().map(|s| s.path_string()),
|
||||
err.error(),
|
||||
|
@ -222,10 +223,10 @@ impl CameraPaintable {
|
|||
}
|
||||
|
||||
pub fn close_pipeline(&self) {
|
||||
log::debug!("Closing pipeline");
|
||||
tracing::debug!("Closing pipeline");
|
||||
if let Some(pipeline) = self.imp().pipeline.borrow_mut().take() {
|
||||
if let Err(err) = pipeline.set_state(gst::State::Null) {
|
||||
log::error!("Failed to close the pipeline: {err}");
|
||||
tracing::error!("Failed to close the pipeline: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +234,7 @@ impl CameraPaintable {
|
|||
pub fn start(&self) {
|
||||
if let Some(pipeline) = &*self.imp().pipeline.borrow() {
|
||||
if let Err(err) = pipeline.set_state(gst::State::Playing) {
|
||||
log::error!("Failed to start the camera stream: {err}");
|
||||
tracing::error!("Failed to start the camera stream: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +242,7 @@ impl CameraPaintable {
|
|||
pub fn stop(&self) {
|
||||
if let Some(pipeline) = &*self.imp().pipeline.borrow() {
|
||||
if let Err(err) = pipeline.set_state(gst::State::Null) {
|
||||
log::error!("Failed to stop the camera stream: {err}");
|
||||
tracing::error!("Failed to stop the camera stream: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{config, models::Keyring, widgets::ErrorRevealer};
|
||||
use crate::{config, models::keyring, utils::spawn_tokio, widgets::ErrorRevealer};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
|
@ -178,7 +178,10 @@ impl PasswordPage {
|
|||
actions,
|
||||
"save_password",
|
||||
clone!(@weak self as page => move |_, _| {
|
||||
page.save();
|
||||
let ctx = glib::MainContext::default();
|
||||
ctx.spawn_local(clone!(@weak page => async move {
|
||||
page.save().await;
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -186,7 +189,10 @@ impl PasswordPage {
|
|||
actions,
|
||||
"reset_password",
|
||||
clone!(@weak self as page => move |_,_| {
|
||||
page.reset_password();
|
||||
let ctx = glib::MainContext::default();
|
||||
ctx.spawn_local(clone!(@weak page => async move {
|
||||
page.reset_password().await;
|
||||
}));
|
||||
})
|
||||
);
|
||||
get_action!(actions, @save_password).set_enabled(false);
|
||||
|
@ -200,18 +206,23 @@ impl PasswordPage {
|
|||
.build();
|
||||
}
|
||||
|
||||
fn reset_password(&self) {
|
||||
async fn reset_password(&self) {
|
||||
let imp = self.imp();
|
||||
|
||||
let current_password = imp.current_password_entry.text();
|
||||
|
||||
if self.has_set_password()
|
||||
&& !Keyring::is_current_password(¤t_password).unwrap_or(false)
|
||||
{
|
||||
let is_current_password =
|
||||
spawn_tokio(async move { keyring::is_current_password(¤t_password).await })
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if self.has_set_password() && !is_current_password {
|
||||
imp.error_revealer.popup(&gettext("Wrong Passphrase"));
|
||||
return;
|
||||
}
|
||||
if Keyring::reset_password().is_ok() {
|
||||
|
||||
let password_was_reset =
|
||||
spawn_tokio(async move { keyring::reset_password().await.is_ok() }).await;
|
||||
|
||||
if password_was_reset {
|
||||
let imp = self.imp();
|
||||
let actions = imp.actions.get().unwrap();
|
||||
|
||||
|
@ -229,21 +240,26 @@ impl PasswordPage {
|
|||
imp.confirm_password_entry.get().set_text("");
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
async fn save(&self) {
|
||||
let imp = self.imp();
|
||||
let actions = imp.actions.get().unwrap();
|
||||
|
||||
let current_password = imp.current_password_entry.text();
|
||||
let password = imp.password_entry.text();
|
||||
let is_current_password = spawn_tokio(async move {
|
||||
keyring::is_current_password(¤t_password)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.await;
|
||||
|
||||
if self.has_set_password()
|
||||
&& !Keyring::is_current_password(¤t_password).unwrap_or(false)
|
||||
{
|
||||
if self.has_set_password() && is_current_password {
|
||||
imp.error_revealer.popup(&gettext("Wrong Passphrase"));
|
||||
return;
|
||||
}
|
||||
|
||||
if Keyring::set_password(&password).is_ok() {
|
||||
let password_was_set =
|
||||
spawn_tokio(async move { keyring::set_password(&password).await.is_ok() }).await;
|
||||
if password_was_set {
|
||||
self.reset();
|
||||
get_action!(actions, @save_password).set_enabled(false);
|
||||
self.set_has_set_password(true);
|
||||
|
|
|
@ -256,7 +256,7 @@ impl PreferencesWindow {
|
|||
win.encyption_key(Operation::Backup, &T::identifier())
|
||||
}).flatten();
|
||||
if let Err(err) = T::backup(&model, &d.file().unwrap(), key.as_deref()) {
|
||||
warn!("Failed to create a backup {}", err);
|
||||
tracing::warn!("Failed to create a backup {}", err);
|
||||
}
|
||||
}
|
||||
d.destroy();
|
||||
|
@ -325,7 +325,7 @@ impl PreferencesWindow {
|
|||
win.restore_items::<T, T::Item>(items);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Failed to parse the selected file {}", err);
|
||||
tracing::warn!("Failed to parse the selected file {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -355,7 +355,7 @@ impl PreferencesWindow {
|
|||
.filter(Result::is_ok)
|
||||
.for_each(|item| {
|
||||
if let Err(err) = item {
|
||||
warn!("Failed to restore item {}", err);
|
||||
tracing::warn!("Failed to restore item {}", err);
|
||||
}
|
||||
});
|
||||
self.emit_by_name::<()>("restore-completed", &[]);
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::models::{Provider, FAVICONS_PATH};
|
|||
use glib::{clone, Receiver, Sender};
|
||||
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
use gtk_macros::send;
|
||||
use tracing::error;
|
||||
|
||||
pub enum ImageAction {
|
||||
Ready(String),
|
||||
|
@ -205,7 +206,7 @@ impl ProviderImage {
|
|||
sender.send(Some(cache_name)).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to load favicon {}", err);
|
||||
tracing::error!("Failed to load favicon {}", err);
|
||||
sender.send(None).unwrap();
|
||||
}
|
||||
};
|
||||
|
@ -223,7 +224,7 @@ impl ProviderImage {
|
|||
send!(imp.sender.clone(), ImageAction::Failed);
|
||||
},
|
||||
Err(_) => {
|
||||
log::debug!("Provider image fetching aborted");
|
||||
tracing::debug!("Provider image fetching aborted");
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
@ -283,7 +284,7 @@ impl ProviderImage {
|
|||
if let Some(provider) = self.provider() {
|
||||
let guard = provider.freeze_notify();
|
||||
if let Err(err) = provider.set_image_uri(&image_path) {
|
||||
warn!("Failed to update provider image {}", err);
|
||||
tracing::warn!("Failed to update provider image {}", err);
|
||||
}
|
||||
drop(guard);
|
||||
}
|
||||
|
|
|
@ -103,12 +103,12 @@ mod imp {
|
|||
|
||||
klass.install_action("providers.save", None, move |page, _, _| {
|
||||
if let Err(err) = page.save() {
|
||||
warn!("Failed to save provider {}", err);
|
||||
tracing::warn!("Failed to save provider {}", err);
|
||||
}
|
||||
});
|
||||
klass.install_action("providers.delete", None, move |page, _, _| {
|
||||
if let Err(err) = page.delete_provider() {
|
||||
warn!("Failed to delete the provider {}", err);
|
||||
tracing::warn!("Failed to delete the provider {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -268,13 +268,13 @@ impl ProviderPage {
|
|||
// Create a 96x96 & 32x32 variants
|
||||
let stream = file.read(gio::Cancellable::NONE)?;
|
||||
let pixbuf = gdk_pixbuf::Pixbuf::from_stream(&stream, gio::Cancellable::NONE)?;
|
||||
log::debug!("Creating a 32x32 variant of the selected favicon");
|
||||
tracing::debug!("Creating a 32x32 variant of the selected favicon");
|
||||
let small_pixbuf = pixbuf
|
||||
.scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
|
||||
.unwrap();
|
||||
small_pixbuf.savev(FAVICONS_PATH.join(small_icon_name), "png", &[])?;
|
||||
|
||||
log::debug!("Creating a 96x96 variant of the selected favicon");
|
||||
tracing::debug!("Creating a 96x96 variant of the selected favicon");
|
||||
let large_pixbuf = pixbuf
|
||||
.scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
|
||||
.unwrap();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::{
|
||||
application::Application,
|
||||
config,
|
||||
models::{Account, Keyring, OTPUri, ProvidersModel},
|
||||
models::{keyring, Account, OTPUri, ProvidersModel},
|
||||
utils::spawn_tokio_blocking,
|
||||
widgets::{
|
||||
accounts::AccountDetailsPage,
|
||||
providers::{ProvidersList, ProvidersListView},
|
||||
|
@ -247,7 +248,7 @@ impl Window {
|
|||
// save window state on delete event
|
||||
self.connect_close_request(move |window| {
|
||||
if let Err(err) = window.save_window_state() {
|
||||
warn!("Failed to save window state {:#?}", err);
|
||||
tracing::warn!("Failed to save window state {:#?}", err);
|
||||
}
|
||||
Inhibit(false)
|
||||
});
|
||||
|
@ -326,10 +327,11 @@ impl Window {
|
|||
"unlock",
|
||||
clone!(@weak self as win, @weak password_entry, @weak app => move |_, _| {
|
||||
let password = password_entry.text();
|
||||
let is_current_password = Keyring::is_current_password(&password).unwrap_or_else(|err| {
|
||||
debug!("Could not verify password: {:?}", err);
|
||||
let is_current_password = spawn_tokio_blocking(async move {
|
||||
keyring::is_current_password(&password).await.unwrap_or_else(|err| {
|
||||
tracing::debug!("Could not verify password: {:?}", err);
|
||||
false
|
||||
});
|
||||
})});
|
||||
if is_current_password {
|
||||
password_entry.set_text("");
|
||||
app.set_locked(false);
|
||||
|
|
Loading…
Add table
Reference in a new issue