// #![cfg_attr(not(feature = "simulator"), no_main)] use core::{cell::RefCell, ffi::CStr, pin::Pin}; use alloc::{ borrow::Cow, boxed::Box, ffi::CString, format, rc::Rc, string::{String, ToString}, vec, vec::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, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem, VecModel, }; use spectre_api_sys::{ SpectreAlgorithm, SpectreCounter, SpectreKeyID, SpectreKeyPurpose, SpectreResultType, SpectreUserKey, }; #[cfg(feature = "limit-fps")] use crate::FRAME_DURATION_MIN; use crate::{ PSRAM_ALLOCATOR, db::{ AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid, ReadTransactionExt, }, ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY}, proxy::OUTPUT_STRING_CHANNEL, ui::{ backend::SlintBackend, dpi::FRAMES_SKIPPED, messages::{ CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUserSites, CallbackMessageUsers, LoginResult, }, storage::{SpectreSite, SpectreSiteConfig, SpectreUserConfig, SpectreUsersConfig}, }, util::DurationExt, }; pub mod backend; pub mod dpi; pub mod messages; pub mod storage; pub mod window_adapter; slint::include_modules!(); fn spectre_derive_user_key( username: &CStr, password: &CStr, encrypted_key: Option, ) -> SpectreUserKey { if let Some(encrypted_key) = encrypted_key { critical_section::with(|cs| { ACTIVE_ENCRYPTED_USER_KEY.borrow(cs).set(encrypted_key); }); } let user_key_start = Instant::now(); unsafe { let user_key = &*spectre_api_sys::spectre_user_key( username.as_ptr(), password.as_ptr(), SpectreAlgorithm::Current, ); let user_key_duration = Instant::now().duration_since(user_key_start); warn!( "User key derived in {} seconds:\n{user_key:02x?}", user_key_duration.display_as_secs() ); let user_key_stack = *user_key; // TODO: Erase memory before freeing __spre_free(user_key as *const _ as *mut _); user_key_stack } } 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; // TODO: // * Store a config as a versioned postcard-serialized struct // * Store accounts and sites as ranges in the DB i_slint_core::properties::ALLOCATOR .set(&PSRAM_ALLOCATOR) .ok() .unwrap(); slint::platform::set_platform(Box::new(backend)).expect("backend already initialized"); let main = AppWindow::new().unwrap(); let state = State::new(db, main).await; let window = state.borrow().window.clone_strong(); State::run_event_loop(window).await; } struct State { window: AppWindow, db: Rc, users: SpectreUsersConfig, /// Currently active view. view: AppState, // Retained state for each view. state_login: StateLogin, state_users: StateUsers, state_user_edit: StateUserEdit, state_user_sites: Option, } impl State { async fn new(db: AcidDatabase, main: AppWindow) -> Rc> { let users = Self::load_users(&db).await; let usernames = users.users.clone().map(|user| user.username); let state = Rc::new(RefCell::new(State { window: main.clone_strong(), users, db: Rc::new(db), view: AppState::Login, state_login: Default::default(), state_users: Default::default(), state_user_edit: Default::default(), state_user_sites: Default::default(), })); main.on_enter_view({ let state = state.clone(); move |view| { state.borrow_mut().set_view(view, true, true); } }); main.on_escape({ let state = state.clone(); move || { State::process_callback_message(&state, CallbackMessage::Escape); } }); main.set_login_usernames(ModelRc::new(usernames)); main.on_login_pw_accepted({ let state = state.clone(); move |user_index, username, password| { State::process_callback_message( &state, CallbackMessage::Login(CallbackMessageLogin::PwAccepted { user_index, username, password, }), ); } }); main.on_login_test_string_accepted(|string| { slint::spawn_local(async move { OUTPUT_STRING_CHANNEL.send(string.to_string()).await; }) .unwrap(); }); main.on_users_edit_user({ let state = state.clone(); move |username, new| { State::process_callback_message( &state, CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }), ); } }); main.on_user_edit_compute_identicon({ let state = state.clone(); move |password| { State::process_callback_message( &state, CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password, }), ); } }); main.on_user_edit_compute_key_id({ let state = state.clone(); move |key| { State::process_callback_message( &state, CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId { key }), ); } }); main.on_user_edit_confirm({ let state = state.clone(); move |_encrypted_key| { State::process_callback_message( &state, 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, }), ); } }); // let sites = Rc::new(VecModel::default()); // sites.push("First".into()); // sites.push("Second".into()); // main.set_sites(ModelRc::new(ModelRc::new(sites.clone()).map( // |mut site: StandardListViewItem| { // site.text += "10"; // site // }, // ))); state } async fn load_users(db: &AcidDatabase) -> SpectreUsersConfig { let read = db.read_transaction().await; let mut buffer = vec![0_u8; 128]; match read .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer) .await { Ok(bytes) => postcard::from_bytes::(bytes).unwrap(), Err(ekv::ReadError::KeyNotFound) => Default::default(), Err(error) => panic!("Failed to read the users config: {error:?}"), } } async fn save_users(db: &AcidDatabase, users: &SpectreUsersConfig) { let mut write = db.write_transaction().await; let buffer = postcard::to_allocvec(&users).unwrap(); write .write(&DbKey::new(DbPathSpectreUsers), &buffer) .await .unwrap(); 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 { AppState::Login => StateLogin::process_callback_message(state_rc, message), AppState::Users => StateUsers::process_callback_message(state_rc, message), AppState::UserEdit => StateUserEdit::process_callback_message(state_rc, message), AppState::UserSites => StateUserSites::process_callback_message(state_rc, message), } } fn reset_view(&mut self) { match self.view { AppState::Login => self.state_login = Default::default(), AppState::Users => self.state_users = Default::default(), AppState::UserEdit => self.state_user_edit = Default::default(), AppState::UserSites => self.state_user_sites = Default::default(), } } fn set_view(&mut self, view: AppState, reset_source: bool, reset_target: bool) { if reset_source { self.reset_view(); } self.view = view; self.window.set_app_state(view); if reset_target { self.reset_view(); } } /// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve /// async by having only one iteration of the loop run, and `await`ing here. /// The following block is analogous to `main.run()`. async fn run_event_loop(window: AppWindow) -> ! { window.show().unwrap(); loop { slint::run_event_loop().unwrap(); // SIGNAL_LCD_SUBMIT.signal(()); #[cfg(feature = "limit-fps")] embassy_time::Timer::after(FRAME_DURATION_MIN).await; let frames_skipped = FRAMES_SKIPPED.wait().await; if frames_skipped > 0 { error!("Renderer missed {frames_skipped} frames."); } // SIGNAL_UI_RENDER.wait().await; } #[expect(unreachable_code)] window.hide().unwrap(); } } trait AppViewTrait { fn process_callback_message(_state_rc: &Rc>, _message: CallbackMessage) {} } #[derive(Default)] struct StateLogin {} impl AppViewTrait for StateLogin { fn process_callback_message(state_rc: &Rc>, message: CallbackMessage) { let mut state = state_rc.borrow_mut(); 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 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 { 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); } _ => (), } } } #[derive(Default)] struct StateUsers {} impl AppViewTrait for StateUsers { fn process_callback_message(state_rc: &Rc>, message: CallbackMessage) { let mut state = state_rc.borrow_mut(); match message { CallbackMessage::Escape => { state.set_view(AppState::Login, true, false); } CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => { state.state_user_edit = StateUserEdit { username: username.clone(), new, password: None, encrypted_key: None, }; state.window.set_user_edit_username(username); state.set_view(AppState::UserEdit, true, false); } _ => (), } } } #[derive(Default)] struct StateUserEdit { username: SharedString, new: bool, password: Option, encrypted_key: Option<(Key, SpectreUserKey)>, } impl AppViewTrait for StateUserEdit { 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::Users, true, false); } CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password }) => { let username_c = CString::new(&*state.state_user_edit.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); let identicon: SharedString = unsafe { let identicon = spectre_api_sys::spectre_identicon( username_c.as_ptr(), password_c.as_ptr(), ); // TODO: identicon.color format!( "{}{}{}{}", CStr::from_ptr(identicon.leftArm).to_str().unwrap(), CStr::from_ptr(identicon.body).to_str().unwrap(), CStr::from_ptr(identicon.rightArm).to_str().unwrap(), CStr::from_ptr(identicon.accessory).to_str().unwrap() ) .into() }; warn!("Identicon: {identicon} ({identicon:?})"); state.window.set_user_edit_identicon(identicon.clone()); state.state_user_edit.password = Some(password); } CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId { key: key_string, }) => { let Some(password) = state.state_user_edit.password.as_ref() else { warn!("Attempted to compute a key ID when no password has been entered."); return; }; let mut key: Key = [0; _]; if let Err(key_decode_error) = hex::decode_to_slice(&*key_string, &mut key) { let message = match key_decode_error { FromHexError::InvalidStringLength | FromHexError::OddLength => { let required_size = key.len() * 2; let provided_size = key_string.len(); let delta = provided_size as i32 - required_size as i32; if delta < 0 { slint::format!("Missing {} characters.", -delta) } else if delta > 0 { slint::format!("{} too many characters.", delta) } else { slint::format!("Invalid key length.") } } FromHexError::InvalidHexCharacter { c, index } => { slint::format!("Invalid character {c:?} at position {index}.") } }; state.window.set_user_edit_key_error(message); return; } let username_c = CString::new(&*state.state_user_edit.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(key)); state.window.set_user_edit_key_error(SharedString::new()); state.window.set_user_edit_key_id( CStr::from_bytes_with_nul(&user_key.keyID.hex) .unwrap() .to_str() .unwrap() .into(), ); state.state_user_edit.encrypted_key = Some((key, user_key)); } CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest) => { let Some(( encrypted_key, SpectreUserKey { keyID: SpectreKeyID { bytes: key_id, .. }, .. }, )) = state.state_user_edit.encrypted_key.take() else { warn!("Encrypted key is not set."); return; }; // If a user with that username already exists, overwrite it. let user = SpectreUserConfig { username: state.state_user_edit.username.clone(), encrypted_key, key_id, }; let mut existing_index = None; for index in 0..state.users.users.row_count() { if let Some(current_user) = state.users.users.row_data(index) && current_user.username == user.username { existing_index = Some(index); } } if let Some(existing_index) = existing_index { state.users.users.set_row_data(existing_index, user); } else { state.users.users.push(user); } slint::spawn_local({ 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( &state_rc, CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed), ); } }) .unwrap(); } CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed) => { state.state_user_edit = Default::default(); state.set_view(AppState::Users, true, true); } _ => (), } } } struct StateUserSites { username: SharedString, user_key: SpectreUserKey, query: SharedString, sites: Rc>, site_list: Rc>, } 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:?}"); slint::spawn_local({ let username = user_sites.username.clone(); let db = state.db.clone(); async move { // Send password to the host. OUTPUT_STRING_CHANNEL.send(site_password).await; // Update the stored site. let mut write = db.write_transaction().await; let key = DbKey::new(DbPathSpectreUserSite { user_sites: DbPathSpectreUserSites { username: Cow::Borrowed(&username), }, site: Cow::Borrowed(&site_name), }); let site = SpectreSiteConfig::default(); let site_bytes = postcard::to_extend(&site, Vec::::new_in(&PSRAM_ALLOCATOR)) .unwrap(); write.write(&key, &site_bytes).await.unwrap(); write.commit().await.unwrap(); } }) .unwrap(); } _ => (), } } } 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(), } } }