From 1e2d43a628cc4bed996ed052bdffb4a62329be21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hlusi=C4=8Dka?= Date: Sun, 8 Feb 2026 21:04:28 +0100 Subject: [PATCH] Site password derivation --- firmware/acid-firmware/src/ui/messages.rs | 25 +- firmware/acid-firmware/src/ui/mod.rs | 536 +++++++++++++----- firmware/acid-firmware/ui/main.slint | 9 +- .../acid-firmware/ui/user-sites-view.slint | 38 +- firmware/password-hash/.cargo/config.toml | 2 + firmware/password-hash/README.md | 14 + firmware/password-hash/build.rs | 2 +- 7 files changed, 465 insertions(+), 161 deletions(-) create mode 100644 firmware/password-hash/.cargo/config.toml diff --git a/firmware/acid-firmware/src/ui/messages.rs b/firmware/acid-firmware/src/ui/messages.rs index c8e5d33..d7924ad 100644 --- a/firmware/acid-firmware/src/ui/messages.rs +++ b/firmware/acid-firmware/src/ui/messages.rs @@ -1,4 +1,8 @@ -use slint::SharedString; +use alloc::rc::Rc; +use slint::{SharedString, VecModel}; +use spectre_api_sys::SpectreUserKey; + +use crate::ui::storage::SpectreSite; pub enum CallbackMessage { /// The escape key was pressed. @@ -9,12 +13,24 @@ pub enum CallbackMessage { UserSites(CallbackMessageUserSites), } +pub enum LoginResult { + Failure, + Success { + user_key: SpectreUserKey, + sites: Rc>, + }, +} + pub enum CallbackMessageLogin { PwAccepted { user_index: i32, username: SharedString, password: SharedString, }, + LoginResult { + username: SharedString, + result: LoginResult, + }, } pub enum CallbackMessageUsers { @@ -24,8 +40,11 @@ pub enum CallbackMessageUsers { pub enum CallbackMessageUserEdit { ComputeIdenticon { password: SharedString }, ComputeKeyId { key: SharedString }, - ConfirmRequest { encrypted_key: SharedString }, + ConfirmRequest, ConfirmProcessed, } -pub enum CallbackMessageUserSites {} +pub enum CallbackMessageUserSites { + SiteNameEdited { query: SharedString }, + SiteNameAccepted { site_list_index: i32 }, +} diff --git a/firmware/acid-firmware/src/ui/mod.rs b/firmware/acid-firmware/src/ui/mod.rs index bcc1024..9fe7441 100644 --- a/firmware/acid-firmware/src/ui/mod.rs +++ b/firmware/acid-firmware/src/ui/mod.rs @@ -1,27 +1,46 @@ // #![cfg_attr(not(feature = "simulator"), no_main)] -use core::{cell::RefCell, ffi::CStr, ops::DerefMut}; +use core::{cell::RefCell, ffi::CStr, ops::DerefMut, pin::Pin}; -use alloc::{boxed::Box, ffi::CString, format, rc::Rc, vec}; +use alloc::{ + borrow::Cow, + boxed::Box, + ffi::CString, + format, + rc::Rc, + string::{String, ToString}, + vec, +}; use embassy_time::Instant; use hex::FromHexError; +use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer}; use log::{error, info, warn}; use password_hash::Key; -use slint::{Model, ModelExt, ModelRc, SharedString, VecModel}; -use spectre_api_sys::{SpectreAlgorithm, SpectreKeyID, SpectreUserKey}; +use slint::{ + Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem, + VecModel, +}; +use spectre_api_sys::{ + SpectreAlgorithm, SpectreCounter, SpectreKeyID, SpectreKeyPurpose, SpectreResultType, + SpectreSiteKey, SpectreUserKey, +}; #[cfg(feature = "limit-fps")] use crate::FRAME_DURATION_MIN; use crate::{ PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, - db::{AcidDatabase, DbKey, DbPathSpectreUsers, PartitionAcid, ReadTransactionExt}, + db::{ + AcidDatabase, DbKey, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid, + ReadTransactionExt, + }, ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY}, ui::{ backend::SlintBackend, messages::{ - CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers, + CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, + CallbackMessageUserSites, CallbackMessageUsers, LoginResult, }, - storage::{SpectreUserConfig, SpectreUsersConfig}, + storage::{SpectreSite, SpectreSiteConfig, SpectreUserConfig, SpectreUsersConfig}, }, util::DurationExt, }; @@ -66,95 +85,30 @@ fn spectre_derive_user_key( } } +fn spectre_derive_site_password(user_key: &SpectreUserKey, site_name: &CStr) -> String { + unsafe { + let site_password_c = &*spectre_api_sys::spectre_site_result( + user_key as *const SpectreUserKey, + site_name.as_ptr(), + SpectreResultType::SpectreResultDefaultResult, + core::ptr::null(), + SpectreCounter::Initial, + SpectreKeyPurpose::Authentication, + core::ptr::null(), + ); + let site_password = CStr::from_ptr(site_password_c) + .to_str() + .unwrap() + .to_string(); + __spre_free(site_password_c as *const _ as *mut _); + site_password + } +} + #[embassy_executor::task] pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) { let db = AcidDatabase::mount(flash_part_acid).await; - // let value = SpectreUsersConfig { - // users: vec![SpectreUserConfig { - // username: "test".to_string(), - // encrypted_key: [0; _], - // key_id: [0; _], - // }], - // }; - - // { - // let mut write = db.write_transaction().await; - // write - // .write( - // &DbKey::new(DbPathSpectreUserSite { - // user_sites: DbPathSpectreUserSites { - // username: "test".into(), - // }, - // site: "example.org".into(), - // }), - // &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(), - // ) - // .await - // .unwrap(); - // write - // .write( - // &DbKey::new(DbPathSpectreUserSite { - // user_sites: DbPathSpectreUserSites { - // username: "test".into(), - // }, - // site: "sub.example.org".into(), - // }), - // &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(), - // ) - // .await - // .unwrap(); - // write - // .write( - // &DbKey::new(DbPathSpectreUsers), - // &postcard::to_allocvec(&value).unwrap(), - // ) - // .await - // .unwrap(); - // write.commit().await.unwrap(); - // } - - // let read_value = { - // let read = db.read_transaction().await; - // // TODO: https://github.com/embassy-rs/ekv/issues/20 - // let mut buffer = vec![0; 256]; - // let slice = read - // .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer) - // .await - // .unwrap(); - // let read_value = postcard::from_bytes::(&slice).unwrap(); - - // let key_sites = DbKey::new(DbPathSpectreUserSites { - // username: "test".into(), - // }); - // let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE]; - // let mut cursor = read - // .read_range(key_sites.range_of_children()) - // .await - // .unwrap(); - - // while let Some((key_len, value_len)) = - // cursor.next(&mut key_buffer, &mut buffer).await.unwrap() - // { - // let key = DbKey::from_raw(key_buffer[..key_len].into()); - // let mut key_segments = key.segments(); - // let value = &buffer[..value_len]; - // let site_config = postcard::from_bytes::(&value).unwrap(); - // let site = SpectreSite { - // config: site_config, - // username: key_segments.nth(2).unwrap().into(), - // site_name: key_segments.nth(1).unwrap().into(), - // }; - - // info!("site = {:#?}", site); - // } - - // read_value - // }; - - // info!("read_value = {:#?}", read_value); - // assert_eq!(value, read_value, "values do not match"); - // TODO: // * Store a config as a versioned postcard-serialized struct // * Store accounts and sites as ranges in the DB @@ -269,11 +223,31 @@ impl State { main.on_user_edit_confirm({ let state = state.clone(); - move |encrypted_key| { + move |_encrypted_key| { State::process_callback_message( &state, - CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest { - encrypted_key, + CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest), + ); + } + }); + + main.on_user_sites_site_name_edited({ + let state = state.clone(); + move |query| { + State::process_callback_message( + &state, + CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }), + ); + } + }); + + main.on_user_sites_site_name_accepted({ + let state = state.clone(); + move |site_list_index| { + State::process_callback_message( + &state, + CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted { + site_list_index, }), ); } @@ -315,6 +289,44 @@ impl State { write.commit().await.unwrap(); } + fn make_site_list( + sites: &Rc>, + query: &SharedString, + ) -> Rc> { + let site_list = Rc::new(VecModel::default()); + + for site in sites.iter() { + site_list.push(SiteListEntry::Existing(site.site_name)); + } + + if !query.is_empty() { + site_list.push(SiteListEntry::New(query.clone())) + } + + site_list + } + + fn update_site_list(&mut self) { + if let Some(StateUserSites { + sites, + query, + site_list, + .. + }) = self.state_user_sites.as_mut() + { + *site_list = State::make_site_list(sites, query); + self.window + .set_user_sites_sites(ModelRc::from(Rc::new(site_list.clone().map(|site| { + let mut item = StandardListViewItem::default(); + item.text = site.to_string(); + item + })))); + } else { + self.window.invoke_user_sites_site_name_clear(); + self.window.set_user_sites_sites(Default::default()); + } + } + fn process_callback_message(state_rc: &Rc>, message: CallbackMessage) { let view = state_rc.borrow().view; match view { @@ -376,49 +388,149 @@ struct StateLogin {} impl AppViewTrait for StateLogin { fn process_callback_message(state_rc: &Rc>, message: CallbackMessage) { let mut state = state_rc.borrow_mut(); - if let CallbackMessage::Login(CallbackMessageLogin::PwAccepted { - user_index, - username: _, - password, - }) = message - { - let Some(user) = state.users.users.row_data(user_index as usize) else { - error!("Failed to find a user with index {user_index}."); - return; - }; - let username_c = - CString::new(&*user.username).expect("Username cannot be converted to a C string."); - let password_c = - CString::new(&*password).expect("Password cannot be converted to a C string."); - let user_key = - spectre_derive_user_key(&username_c, &password_c, Some(user.encrypted_key)); + match message { + CallbackMessage::Login(CallbackMessageLogin::PwAccepted { + user_index, + username: _, + password, + }) => { + let Some(user) = state.users.users.row_data(user_index as usize) else { + error!("Failed to find a user with index {user_index}."); + return; + }; + let username_c = CString::new(&*user.username) + .expect("Username cannot be converted to a C string."); + let password_c = + CString::new(&*password).expect("Password cannot be converted to a C string."); + let user_key = + spectre_derive_user_key(&username_c, &password_c, Some(user.encrypted_key)); - // let site_key_start = Instant::now(); - // let site_key = &*spectre_api_sys::spectre_site_key( - // user_key as *const SpectreUserKey, - // c"example.org".as_ptr(), - // SpectreCounter::Initial, - // SpectreKeyPurpose::Authentication, - // c"".as_ptr(), - // ); - // let site_key_duration = Instant::now().duration_since(site_key_start); - // warn!( - // "Site key derived in {} seconds:\n{site_key:02x?}", - // site_key_duration.display_as_secs() - // ); - // TODO: Erase memory before freeing - // __spre_free(site_key as *const _ as *mut _); + // { + // let mut write = db.write_transaction().await; + // write + // .write( + // &DbKey::new(DbPathSpectreUserSite { + // user_sites: DbPathSpectreUserSites { + // username: "test".into(), + // }, + // site: "example.org".into(), + // }), + // &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(), + // ) + // .await + // .unwrap(); + // write + // .write( + // &DbKey::new(DbPathSpectreUserSite { + // user_sites: DbPathSpectreUserSites { + // username: "test".into(), + // }, + // site: "sub.example.org".into(), + // }), + // &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(), + // ) + // .await + // .unwrap(); + // write + // .write( + // &DbKey::new(DbPathSpectreUsers), + // &postcard::to_allocvec(&value).unwrap(), + // ) + // .await + // .unwrap(); + // write.commit().await.unwrap(); + // } - if user.key_id == user_key.keyID.bytes { - info!("Correct password entered for user {:?}.", user.username); - state.state_user_sites = Some(StateUserSites { - username: user.username, - user_key, - }); - state.set_view(AppState::UserSites, true, false); - } else { - warn!("Incorrect password entered for user {:?}.", user.username); + if user.key_id != user_key.keyID.bytes { + State::process_callback_message( + &state_rc, + CallbackMessage::Login(CallbackMessageLogin::LoginResult { + username: user.username, + result: LoginResult::Failure, + }), + ); + return; + } + + slint::spawn_local({ + let state_rc = state_rc.clone(); + let username = user.username.clone(); + let db = state.db.clone(); + + async move { + let read = db.read_transaction().await; + // TODO: https://github.com/embassy-rs/ekv/issues/20 + let mut buffer = vec![0; 256]; + // let slice = read + // .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer) + // .await + // .unwrap(); + // let read_value = postcard::from_bytes::(&slice).unwrap(); + + let key_sites = DbKey::new(DbPathSpectreUserSites { + username: Cow::Borrowed(&username), + }); + let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE]; + let mut cursor = read + .read_range(key_sites.range_of_children()) + .await + .unwrap(); + let sites = VecModel::default(); + + while let Some((key_len, value_len)) = + cursor.next(&mut key_buffer, &mut buffer).await.unwrap() + { + let key = DbKey::from_raw(key_buffer[..key_len].into()); + let mut key_segments = key.segments(); + let value = &buffer[..value_len]; + let site_config = + postcard::from_bytes::(&value).unwrap(); + let site = SpectreSite { + config: site_config, + username: key_segments.nth(2).unwrap().as_ref().into(), + site_name: key_segments.nth(1).unwrap().as_ref().into(), + }; + + info!("site = {:#?}", site); + sites.push(site); + } + + State::process_callback_message( + &state_rc, + CallbackMessage::Login(CallbackMessageLogin::LoginResult { + username: user.username, + result: LoginResult::Success { + user_key, + sites: Rc::new(sites), + }, + }), + ); + } + }) + .unwrap(); } + CallbackMessage::Login(CallbackMessageLogin::LoginResult { + username, + result: LoginResult::Success { user_key, sites }, + }) => { + info!("Correct password entered for user {:?}.", username); + state.state_user_sites = Some(StateUserSites { + username, + user_key, + query: SharedString::new(), + sites: sites.clone(), + site_list: Default::default(), + }); + state.update_site_list(); + state.set_view(AppState::UserSites, true, false); + } + CallbackMessage::Login(CallbackMessageLogin::LoginResult { + username, + result: LoginResult::Failure, + }) => { + warn!("Incorrect password entered for user {:?}.", username); + } + _ => (), } } } @@ -538,9 +650,7 @@ impl AppViewTrait for StateUserEdit { ); state.state_user_edit.encrypted_key = Some((key, user_key)); } - CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest { - encrypted_key: _, - }) => { + CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest) => { let Some(( encrypted_key, SpectreUserKey { @@ -580,6 +690,7 @@ impl AppViewTrait for StateUserEdit { let state_rc = state_rc.clone(); let db = state.db.clone(); let users = state.users.clone(); + async move { State::save_users(&db, &users).await; State::process_callback_message( @@ -602,6 +713,153 @@ impl AppViewTrait for StateUserEdit { struct StateUserSites { username: SharedString, user_key: SpectreUserKey, + query: SharedString, + sites: Rc>, + site_list: Rc>, } -impl AppViewTrait for StateUserSites {} +impl AppViewTrait for StateUserSites { + fn process_callback_message(state_rc: &Rc>, message: CallbackMessage) { + let state = state_rc.clone(); + let mut state = state.borrow_mut(); + match message { + CallbackMessage::Escape => { + state.set_view(AppState::Login, true, false); + } + CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }) => { + if let Some(user_sites) = state.state_user_sites.as_mut() { + user_sites.query = query; + state.update_site_list(); + } + } + CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted { + site_list_index, + }) => { + let Some(user_sites) = state.state_user_sites.as_mut() else { + error!("User sites uninitialized."); + return; + }; + let Some(site_list_entry) = user_sites.site_list.row_data(site_list_index as usize) + else { + error!("Invalid site list entry index: {site_list_index}"); + return; + }; + warn!("Site name accepted: {site_list_entry:?}"); + let site_name = match site_list_entry { + SiteListEntry::New(site_name) => site_name, + SiteListEntry::Existing(site_name) => site_name, + }; + let site_name_c = CString::new(&*site_name).unwrap(); + let site_password = + spectre_derive_site_password(&user_sites.user_key, &site_name_c); + + warn!("Site password: {site_password:?}"); + } + _ => (), + } + } +} + +pub struct SitesModel(Pin>>>) +where + M: Model + 'static; + +struct SitesModelInner +where + M: Model + 'static, +{ + wrapped_model: M, + notify: ModelNotify, +} + +impl ModelChangeListener for SitesModelInner +where + M: Model + 'static, +{ + fn row_changed(self: Pin<&Self>, row: usize) { + self.notify + .row_changed(self.wrapped_model.row_count() - 1 - row); + } + + fn row_added(self: Pin<&Self>, index: usize, count: usize) { + let row_count = self.wrapped_model.row_count(); + let old_row_count = row_count - count; + let index = old_row_count - index; + self.notify.row_added(index, count); + } + + fn row_removed(self: Pin<&Self>, index: usize, count: usize) { + let row_count = self.wrapped_model.row_count(); + self.notify.row_removed(row_count - index, count); + } + + fn reset(self: Pin<&Self>) { + self.notify.reset() + } +} + +impl SitesModel +where + M: Model + 'static, +{ + pub fn new(wrapped_model: M) -> Self { + let inner = SitesModelInner { + wrapped_model, + notify: Default::default(), + }; + let container = Box::pin(ModelChangeListenerContainer::new(inner)); + container + .wrapped_model + .model_tracker() + .attach_peer(container.as_ref().model_peer()); + Self(container) + } + + /// Returns a reference to the inner model + pub fn source_model(&self) -> &M { + &self.0.as_ref().get().get_ref().wrapped_model + } +} + +impl Model for SitesModel +where + M: Model + 'static, +{ + type Data = M::Data; + + fn row_count(&self) -> usize { + self.0.wrapped_model.row_count() + } + + fn row_data(&self, row: usize) -> Option { + let count = self.0.wrapped_model.row_count(); + self.0.wrapped_model.row_data(count.checked_sub(row + 1)?) + } + fn set_row_data(&self, row: usize, data: Self::Data) { + let count = self.0.as_ref().wrapped_model.row_count(); + self.0.wrapped_model.set_row_data(count - row - 1, data); + } + + fn model_tracker(&self) -> &dyn ModelTracker { + &self.0.notify + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +#[derive(Clone, Debug)] +pub enum SiteListEntry { + New(SharedString), + Existing(SharedString), +} + +impl SiteListEntry { + pub fn to_string(&self) -> SharedString { + match self { + SiteListEntry::New(site) => slint::format!("{site} "), + SiteListEntry::Existing(site) => site.clone(), + } + } +} diff --git a/firmware/acid-firmware/ui/main.slint b/firmware/acid-firmware/ui/main.slint index 669b694..a2e7a08 100644 --- a/firmware/acid-firmware/ui/main.slint +++ b/firmware/acid-firmware/ui/main.slint @@ -51,9 +51,12 @@ export component AppWindow inherits Window { callback user_edit_compute_key_id <=> user_edit_view.compute_key_id; callback user_edit_confirm <=> user_edit_view.confirm; // User Sites View - in property <[StandardListViewItem]> sites <=> user_sites_view.model; - callback site_pw_edited <=> user_sites_view.pw_edited; - callback site_pw_accepted <=> user_sites_view.pw_accepted; + in property <[StandardListViewItem]> user_sites_sites <=> user_sites_view.model; + callback user_sites_site_name_edited <=> user_sites_view.site_name_edited; + callback user_sites_site_name_accepted <=> user_sites_view.site_name_accepted; + public function user_sites_site_name_clear() { + user_sites_view.site_name_clear(); + } focus-scope := FocusScope { key-pressed(event) => { if event.text == "\u{1b}" { diff --git a/firmware/acid-firmware/ui/user-sites-view.slint b/firmware/acid-firmware/ui/user-sites-view.slint index aaf9183..0ae4d4e 100644 --- a/firmware/acid-firmware/ui/user-sites-view.slint +++ b/firmware/acid-firmware/ui/user-sites-view.slint @@ -7,24 +7,32 @@ export component UserSitesView inherits HorizontalLayout { spacing: Style.spacing; in property <[StandardListViewItem]> model <=> list_view_sites.model; in-out property current-item <=> list_view_sites.current-item; - callback pw_edited <=> line_edit_site_pw.edited; - callback pw_accepted <=> line_edit_site_pw.accepted; - VerticalLayout { - spacing: Style.spacing; - Text { - text: "Send password for:"; + callback site_name_edited <=> line_edit_site_name.edited; + callback site_name_accepted(site_list_index: int); + public function site_name_clear() { + line_edit_site_name.text = ""; + } + FocusScope { + key-pressed(event) => { + if event.text == "\n" { + site_name_accepted(list_view_sites.current-item); + EventResult.accept + } else { + EventResult.reject + } } + VerticalLayout { + spacing: Style.spacing; + Text { + text: "Send password for:"; + } - line_edit_site_pw := LineEdit { - input-type: InputType.text; - placeholder-text: "example.org"; - } + line_edit_site_name := LineEdit { + input-type: InputType.text; + placeholder-text: "example.org"; + } - list_view_sites := StandardListView { - model: [ - { text: "Test" }, - { text: "Test" }, - ]; + list_view_sites := StandardListView { } } } diff --git a/firmware/password-hash/.cargo/config.toml b/firmware/password-hash/.cargo/config.toml new file mode 100644 index 0000000..49c39c6 --- /dev/null +++ b/firmware/password-hash/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] # These must be kept in sync with /.zed/settings.json +LIBSODIUM_INSTALL_DIR = "../libsodium/install-host" diff --git a/firmware/password-hash/README.md b/firmware/password-hash/README.md index 243d24a..296f301 100644 --- a/firmware/password-hash/README.md +++ b/firmware/password-hash/README.md @@ -1,3 +1,17 @@ # Compiling Compile on Linux or in WSL. + +Compile libsodium for the host: +```bash +cd ../libsodium +./configure --disable-shared --enable-static --prefix="$PWD/install-host" +make -j +make install +``` + +Then compile and run password-hash: +```bash +cd ../password-hash +cargo run --release [--target x86_64-unknown-linux-gnu] +``` diff --git a/firmware/password-hash/build.rs b/firmware/password-hash/build.rs index 439f16f..3de6776 100644 --- a/firmware/password-hash/build.rs +++ b/firmware/password-hash/build.rs @@ -29,7 +29,7 @@ fn main() { libsodium_install_dir.join("lib/libsodium.a").display() ); } else { - println!("cargo:warn=Environment variable `LIBSODIUM_INSTALL_DIR` missing!"); + panic!("Environment variable `LIBSODIUM_INSTALL_DIR` missing!"); } } }