secret: Use oo7 instead of libsecret

This commit is contained in:
Kévin Commaille 2022-10-22 10:48:23 +02:00 committed by Kévin Commaille
parent b0f51bd1f9
commit 4d5791f817
9 changed files with 260 additions and 201 deletions

103
Cargo.lock generated
View File

@ -693,6 +693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core 0.6.3",
"typenum",
]
@ -707,9 +708,9 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "3.2.1"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0"
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
dependencies = [
"byteorder",
"digest 0.9.0",
@ -1085,7 +1086,6 @@ dependencies = [
"image 0.24.4",
"indexmap",
"libadwaita",
"libsecret",
"libshumate",
"log",
"matrix-sdk",
@ -1093,6 +1093,7 @@ dependencies = [
"mime_guess",
"num_enum",
"once_cell",
"oo7",
"pulldown-cmark",
"qrcode",
"rand 0.8.5",
@ -2352,34 +2353,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "libsecret"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4af5a2342942fa42d706a424e9f9914287fb8317132750fd73a241140ac38c1"
dependencies = [
"bitflags",
"gio",
"glib",
"libc",
"libsecret-sys",
"once_cell",
]
[[package]]
name = "libsecret-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b630bef24b542dc1609a14c56b9267c147dbef1ee7ad08fb1a852a07c17d492d"
dependencies = [
"gio-sys",
"glib-sys",
"gobject-sys",
"libc",
"pkg-config",
"system-deps",
]
[[package]]
name = "libshumate"
version = "0.1.1"
@ -2890,6 +2863,40 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational 0.4.1",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@ -2929,6 +2936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-bigint",
"num-integer",
"num-traits",
]
@ -3008,6 +3016,32 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "oo7"
version = "0.1.0-alpha.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e614140b3625a16ddde5880e5c6429d4ef901883256edf496cceb62af1c7b6f8"
dependencies = [
"aes",
"byteorder",
"cbc",
"cipher 0.4.3",
"digest 0.10.3",
"dirs",
"futures",
"hkdf",
"hmac",
"num",
"once_cell",
"pbkdf2",
"rand 0.8.5",
"serde",
"sha2 0.10.5",
"tokio",
"zbus",
"zeroize",
]
[[package]]
name = "opaque-debug"
version = "0.3.0"
@ -5040,8 +5074,7 @@ dependencies = [
[[package]]
name = "x25519-dalek"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077"
source = "git+https://github.com/A6GibKm/x25519-dalek?rev=9f19028c34107eea87d37bcee2eb2b350ec34cfe#9f19028c34107eea87d37bcee2eb2b350ec34cfe"
dependencies = [
"curve25519-dalek",
"rand_core 0.5.1",
@ -5126,9 +5159,9 @@ dependencies = [
[[package]]
name = "zeroize"
version = "1.3.0"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
dependencies = [
"zeroize_derive",
]

View File

@ -28,7 +28,9 @@ serde = "1.0.130"
serde_json = "1.0"
tokio = { version = "1.15", features = ["rt", "rt-multi-thread", "sync"] }
url = "2.2"
libsecret = { version = "0.1.4", features = ["v0_19"] }
oo7 = { version = "0.1.0-alpha.5", default-features = false, features = [
"tokio",
] }
html2pango = "0.5.0"
futures = "0.3"
rand = "0.8"
@ -93,3 +95,7 @@ features = [
"unstable-msc3440",
"unstable-sanitize",
]
[patch.crates-io.x25519-dalek]
git = "https://github.com/A6GibKm/x25519-dalek"
rev = "9f19028c34107eea87d37bcee2eb2b350ec34cfe"

View File

@ -26,8 +26,6 @@ dependency('gstreamer-1.0', version: '>= 1.18')
dependency('gstreamer-base-1.0', version: '>= 1.18')
dependency('gstreamer-plugins-base-1.0', version: '>= 1.18')
dependency('gstreamer-video-1.0', version: '>= 1.18')
dependency('libsecret-1', version: '>= 0.19',
default_options: ['gtk_doc=false', 'gir=false', 'vapi=false'])
glib_compile_resources = find_program('glib-compile-resources', required: true)
glib_compile_schemas = find_program('glib-compile-schemas', required: true)

View File

@ -3,7 +3,7 @@ use gettextrs::gettext;
use gtk::{self, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use log::error;
use crate::{secret, secret::SecretError, spawn, toast, window::Window};
use crate::{spawn, toast, window::Window};
pub enum ErrorSubpage {
SecretErrorSession,
@ -33,7 +33,7 @@ mod imp {
pub page: TemplateChild<adw::StatusPage>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
pub secret_error: RefCell<Option<SecretError>>,
pub secret_item: RefCell<Option<oo7::Item>>,
}
#[glib::object_subclass]
@ -82,22 +82,24 @@ impl ErrorPage {
glib::Object::new(&[]).expect("Failed to create ErrorPage")
}
pub fn display_secret_error(&self, message: &str, error: SecretError) {
pub fn display_secret_error(&self, message: &str, item: Option<oo7::Item>) {
let priv_ = self.imp();
self.action_set_enabled(
"error-page.remove-secret-error-session",
matches!(error, SecretError::CorruptSession(_)),
);
self.action_set_enabled("error-page.remove-secret-error-session", item.is_some());
priv_.page.set_description(Some(message));
priv_
.stack
.set_visible_child_name(error.error_subpage().as_ref());
priv_.secret_error.replace(Some(error));
let error_subpage = if item.is_some() {
ErrorSubpage::SecretErrorSession
} else {
ErrorSubpage::SecretErrorOther
};
priv_.stack.set_visible_child_name(error_subpage.as_ref());
priv_.secret_item.replace(item);
}
async fn remove_secret_error_session(&self) {
if let Some(SecretError::CorruptSession((_, item))) = self.imp().secret_error.take() {
match secret::remove_item(&item).await {
if let Some(item) = self.imp().secret_item.take() {
match item.delete().await {
Ok(_) => {
self.action_set_enabled("error-page.remove-secret-error-session", false);
if let Some(window) = self

View File

@ -595,12 +595,16 @@ impl Login {
let session = Session::new();
if is_new {
if let Err(error) = secret::store_session(&session_info).await {
let session_info = session_info.clone();
let handle = spawn_tokio!(async move { secret::store_session(&session_info).await });
if let Err(error) = handle.await.unwrap() {
error!("Couldn't store session: {:?}", error);
let (message, item) = error.into_parts();
self.parent_window().switch_to_error_page(
&format!("{}\n\n{}", gettext("Unable to store session"), error),
error,
&format!("{}\n\n{}", gettext("Unable to store session"), message),
item,
);
return;
}

View File

@ -27,14 +27,8 @@ use gtk::{gdk::Display, gio, IconTheme};
use once_cell::sync::Lazy;
use self::{
application::Application,
error_page::{ErrorPage, ErrorSubpage},
greeter::Greeter,
i18n::*,
login::Login,
session::Session,
user_facing_error::UserFacingError,
window::Window,
application::Application, error_page::ErrorPage, greeter::Greeter, i18n::*, login::Login,
session::Session, user_facing_error::UserFacingError, window::Window,
};
/// The default tokio runtime to be used for async tasks

View File

@ -1,68 +1,104 @@
use std::{collections::HashMap, ffi::OsStr, fmt, path::PathBuf, string::FromUtf8Error};
use gettextrs::gettext;
use gtk::{gio, glib};
use libsecret::{
password_clear_future, password_search_sync, password_store_binary_future, prelude::*,
Retrievable, Schema, SchemaAttributeType, SchemaFlags, SearchFlags, Value, COLLECTION_DEFAULT,
};
use log::error;
use matrix_sdk::ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
use oo7::{is_sandboxed, Item, Keyring};
use serde::{Deserialize, Serialize};
use serde_json::error::Error as JsonError;
use thiserror::Error;
use url::Url;
use crate::{config::APP_ID, gettext_f, ErrorSubpage};
use crate::{config::APP_ID, gettext_f, user_facing_error::UserFacingError};
const SCHEMA_ATTRIBUTE: &str = "xdg:schema";
/// Any error that can happen when interacting with the secret service.
#[derive(Debug, Clone)]
#[derive(Debug, Error)]
pub enum SecretError {
CorruptSession((String, Retrievable)),
Libsecret(glib::Error),
Unknown,
/// A corrupted session was found.
#[error("{0}")]
CorruptSession(String, Item),
/// An error occurred interacting with the secret service.
#[error(transparent)]
Oo7(#[from] oo7::Error),
}
impl SecretError {
/// Get the error subpage that matches `self`.
pub fn error_subpage(&self) -> ErrorSubpage {
/// Split `self` between its message and its optional `Item`.
pub fn into_parts(self) -> (String, Option<Item>) {
match self {
Self::CorruptSession(_) => ErrorSubpage::SecretErrorSession,
_ => ErrorSubpage::SecretErrorOther,
SecretError::CorruptSession(message, item) => (message, Some(item)),
SecretError::Oo7(error) => (error.to_user_facing(), None),
}
}
}
impl From<glib::Error> for SecretError {
fn from(error: glib::Error) -> Self {
Self::Libsecret(error)
impl UserFacingError for oo7::Error {
fn to_user_facing(self) -> String {
match self {
oo7::Error::Portal(error) => error.to_user_facing(),
oo7::Error::DBus(error) => error.to_user_facing(),
}
}
}
impl fmt::Display for SecretError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::CorruptSession((message, _)) => message.to_owned(),
Self::Libsecret(error) if error.is::<libsecret::Error>() => {
match error.kind::<libsecret::Error>() {
Some(libsecret::Error::Protocol) => error.message().to_owned(),
Some(libsecret::Error::IsLocked) => {
gettext("Could not unlock the secret storage")
}
_ => gettext(
"An unknown error occurred when interacting with the secret storage",
),
}
}
_ => gettext("An unknown error occurred when interacting with the secret storage"),
}
)
impl UserFacingError for oo7::portal::Error {
fn to_user_facing(self) -> String {
match self {
oo7::portal::Error::FileHeaderMismatch(_) |
oo7::portal::Error::VersionMismatch(_) |
oo7::portal::Error::NoData |
oo7::portal::Error::MacError |
oo7::portal::Error::HashedAttributeMac(_) |
oo7::portal::Error::GVariantDeserialization(_) => gettext(
"The secret storage file is corrupted.",
),
oo7::portal::Error::NoParentDir(_) |
oo7::portal::Error::NoDataDir => gettext(
"Could not access the secret storage file location.",
),
oo7::portal::Error::Io(_) => gettext(
"An unknown error occurred when accessing the secret storage file.",
),
oo7::portal::Error::TargetFileChanged(_) => gettext(
"The secret storage file has been changed by another process.",
),
oo7::portal::Error::PortalBus(_) => gettext(
"An unknown error occurred when interacting with the D-Bus Secret Service.",
),
oo7::portal::Error::CancelledPortalRequest => gettext(
"The request to the Flatpak Secret Portal was cancelled. Make sure to accept any prompt asking to access it.",
),
oo7::portal::Error::PortalNotAvailable => gettext(
"The Flatpak Secret Portal is not available. Make sure xdg-desktop-portal is installed, and it is at least at version 1.5.0.",
),
}
}
}
#[derive(Debug, Clone)]
impl UserFacingError for oo7::dbus::Error {
fn to_user_facing(self) -> String {
match self {
oo7::dbus::Error::Deleted => gettext(
"The item was deleted.",
),
oo7::dbus::Error::Dismissed => gettext(
"The request to the D-Bus Secret Service was cancelled. Make sure to accept any prompt asking to access it.",
),
oo7::dbus::Error::NotFound(_) => gettext(
"Could not access the default collection. Make sure a keyring was created and set as default.",
),
oo7::dbus::Error::Zbus(_) |
oo7::dbus::Error::IO(_) => gettext(
"An unknown error occurred when interacting with the D-Bus Secret Service.",
),
}
}
}
#[derive(Clone)]
pub struct StoredSession {
pub homeserver: Url,
pub user_id: OwnedUserId,
@ -71,10 +107,21 @@ pub struct StoredSession {
pub secret: Secret,
}
impl fmt::Debug for StoredSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoredSession")
.field("homeserver", &self.homeserver)
.field("user_id", &self.user_id)
.field("device_id", &self.device_id)
.field("path", &self.path)
.finish()
}
}
impl StoredSession {
/// Build self from a secret.
pub async fn try_from_secret_item(item: Retrievable) -> Result<Self, SecretError> {
let attr = item.attributes();
pub async fn try_from_secret_item(item: Item) -> Result<Self, SecretError> {
let attr = item.attributes().await?;
let homeserver = match attr.get("homeserver") {
Some(string) => match Url::parse(string) {
@ -84,17 +131,17 @@ impl StoredSession {
"Could not parse 'homeserver' attribute in stored session: {:?}",
err
);
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Malformed homeserver in stored session"),
item,
)));
));
}
},
None => {
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Could not find homeserver in stored session"),
item,
)));
));
}
};
let user_id = match attr.get("user") {
@ -105,60 +152,54 @@ impl StoredSession {
"Could not parse 'user' attribute in stored session: {:?}",
err
);
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Malformed user ID in stored session"),
item,
)));
));
}
},
None => {
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Could not find user ID in stored session"),
item,
)));
));
}
};
let device_id = match attr.get("device-id") {
Some(string) => <&DeviceId>::from(string.as_str()).to_owned(),
None => {
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Could not find device ID in stored session"),
item,
)));
));
}
};
let path = match attr.get("db-path") {
Some(string) => PathBuf::from(string),
None => {
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Could not find database path in stored session"),
item,
)));
));
}
};
let secret = match item.retrieve_secret_future().await {
Ok(Some(value)) => match Secret::from_utf8(value.get()) {
let secret = match item.secret().await {
Ok(secret) => match Secret::from_utf8(&secret) {
Ok(secret) => secret,
Err(err) => {
error!("Could not parse secret in stored session: {:?}", err);
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Malformed secret in stored session"),
item,
)));
));
}
},
Ok(None) => {
return Err(SecretError::CorruptSession((
gettext("No secret in stored session"),
item,
)));
}
Err(err) => {
error!("Could not get secret in stored session: {:?}", err);
return Err(SecretError::CorruptSession((
return Err(SecretError::CorruptSession(
gettext("Could not get secret in stored session"),
item,
)));
));
}
};
@ -171,20 +212,14 @@ impl StoredSession {
})
}
/// Build a secret from `self`.
///
/// Returns an (attributes, secret) tuple.
pub fn to_secret_item(&self) -> (HashMap<&str, &str>, Value) {
let attributes = HashMap::from([
/// Get the attributes from `self`.
pub fn attributes(&self) -> HashMap<&str, &str> {
HashMap::from([
("homeserver", self.homeserver.as_str()),
("user", self.user_id.as_str()),
("device-id", self.device_id.as_str()),
("db-path", self.path.to_str().unwrap()),
]);
let secret = Value::new(&self.secret.to_string(), "application/json");
(attributes, secret)
])
}
/// Get the unique ID for this `StoredSession`.
@ -219,7 +254,7 @@ impl From<JsonError> for FromUtf8SecretError {
}
/// A `Secret` that can be stored in the `SecretService`.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Clone, Deserialize, Serialize)]
pub struct Secret {
pub access_token: String,
pub passphrase: String,
@ -227,38 +262,24 @@ pub struct Secret {
impl Secret {
/// Converts a vector of bytes to a `Secret`.
pub fn from_utf8(vec: Vec<u8>) -> Result<Self, FromUtf8SecretError> {
let s = String::from_utf8(vec)?;
pub fn from_utf8(slice: &[u8]) -> Result<Self, FromUtf8SecretError> {
let s = String::from_utf8(slice.to_owned())?;
Ok(serde_json::from_str(&s)?)
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap())
}
}
/// The `Schema` of the items in the `SecretService`.
fn schema() -> Schema {
let attributes = HashMap::from([
("homeserver", SchemaAttributeType::String),
("user", SchemaAttributeType::String),
("device-id", SchemaAttributeType::String),
("db-path", SchemaAttributeType::String),
]);
Schema::new(APP_ID, SchemaFlags::NONE, attributes)
}
/// Retrieves all sessions stored to the `SecretService`
pub async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError> {
let items = password_search_sync(
Some(&schema()),
HashMap::new(),
SearchFlags::ALL | SearchFlags::UNLOCK | SearchFlags::LOAD_SECRETS,
gio::Cancellable::NONE,
)?;
let keyring = Keyring::new().await?;
let items = if is_sandboxed() {
keyring.items().await?
} else {
keyring
.search_items(HashMap::from([(SCHEMA_ATTRIBUTE, APP_ID)]))
.await?
};
let mut sessions = Vec::with_capacity(items.len());
for item in items {
@ -271,43 +292,40 @@ pub async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError> {
/// Writes a session to the `SecretService`, overwriting any previously stored
/// session with the same `homeserver`, `username` and `device-id`.
pub async fn store_session(session: &StoredSession) -> Result<(), SecretError> {
let (attributes, secret) = session.to_secret_item();
let keyring = Keyring::new().await?;
password_store_binary_future(
Some(&schema()),
attributes,
Some(&COLLECTION_DEFAULT),
&gettext_f(
// Translators: Do NOT translate the content between '{' and '}', this is a variable
// name.
"Fractal: Matrix credentials for {user_id}",
&[("user_id", session.user_id.as_str())],
),
&secret,
)
.await?;
let mut attributes = session.attributes();
if !is_sandboxed() {
attributes.insert(SCHEMA_ATTRIBUTE, APP_ID);
}
let secret = serde_json::to_string(&session.secret).unwrap();
keyring
.create_item(
&gettext_f(
// Translators: Do NOT translate the content between '{' and '}', this is a
// variable name.
"Fractal: Matrix credentials for {user_id}",
&[("user_id", session.user_id.as_str())],
),
attributes,
secret,
true,
)
.await?;
Ok(())
}
/// Removes a session from the `SecretService`
pub async fn remove_session(session: &StoredSession) -> Result<(), SecretError> {
let (attributes, _) = session.to_secret_item();
let keyring = Keyring::new().await?;
password_clear_future(Some(&schema()), attributes).await?;
Ok(())
}
/// Removes an item from the `SecretService`
pub async fn remove_item(item: &Retrievable) -> Result<(), SecretError> {
let attributes = item.attributes();
let mut attr = HashMap::with_capacity(attributes.len());
for (key, value) in attributes.iter() {
attr.insert(key.as_str(), value.as_str());
}
password_clear_future(Some(&schema()), attr).await?;
let attributes = session.attributes();
keyring.delete(attributes).await?;
Ok(())
}

View File

@ -709,7 +709,9 @@ impl Session {
settings.delete_settings();
}
if let Err(error) = secret::remove_session(info).await {
let session_info = info.clone();
let handle = spawn_tokio!(async move { secret::remove_session(&session_info).await });
if let Err(error) = handle.await.unwrap() {
error!(
"Failed to remove credentials from SecretService after logout: {}",
error

View File

@ -7,8 +7,7 @@ use log::{info, warn};
use crate::{
account_switcher::AccountSwitcher,
config::{APP_ID, PROFILE},
secret::{self, SecretError},
spawn, Application, ErrorPage, Greeter, Login, Session,
secret, spawn, spawn_tokio, Application, ErrorPage, Greeter, Login, Session,
};
mod imp {
@ -212,7 +211,8 @@ impl Window {
}
pub async fn restore_sessions(&self) {
match secret::restore_sessions().await {
let handle = spawn_tokio!(secret::restore_sessions());
match handle.await.unwrap() {
Ok(sessions) => {
if sessions.is_empty() {
self.switch_to_greeter_page();
@ -237,13 +237,15 @@ impl Window {
}
Err(error) => {
warn!("Failed to restore previous sessions: {:?}", error);
let (message, item) = error.into_parts();
self.switch_to_error_page(
&format!(
"{}\n\n{}",
gettext("Failed to restore previous sessions"),
error,
message,
),
error,
item,
);
}
}
@ -316,9 +318,9 @@ impl Window {
priv_.main_stack.set_visible_child(&*priv_.greeter);
}
pub fn switch_to_error_page(&self, message: &str, error: SecretError) {
pub fn switch_to_error_page(&self, message: &str, item: Option<oo7::Item>) {
let priv_ = self.imp();
priv_.error_page.display_secret_error(message, error);
priv_.error_page.display_secret_error(message, item);
priv_.main_stack.set_visible_child(&*priv_.error_page);
}