From 40b9b5d27872a63352dea261d3546908d124d64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hlusi=C4=8Dka?= Date: Wed, 4 Feb 2026 03:14:21 +0100 Subject: [PATCH] Implement spectre user addition --- firmware/.vscode/settings.json | 25 +- firmware/.zed/settings.json | 2 +- firmware/Cargo.lock | 2 + firmware/Cargo.toml | 2 +- firmware/acid-firmware/Cargo.toml | 3 +- firmware/acid-firmware/src/ffi/crypto.rs | 31 ++- firmware/acid-firmware/src/logging.rs | 43 ++- firmware/acid-firmware/src/main.rs | 8 +- firmware/acid-firmware/src/ui/backend.rs | 96 +++++-- firmware/acid-firmware/src/ui/messages.rs | 11 +- firmware/acid-firmware/src/ui/mod.rs | 259 +++++++++++++----- firmware/acid-firmware/src/ui/storage.rs | 11 +- firmware/acid-firmware/ui/main.slint | 3 + .../acid-firmware/ui/user-edit-view.slint | 41 ++- 14 files changed, 386 insertions(+), 151 deletions(-) diff --git a/firmware/.vscode/settings.json b/firmware/.vscode/settings.json index 08f0425..9305930 100644 --- a/firmware/.vscode/settings.json +++ b/firmware/.vscode/settings.json @@ -1,4 +1,25 @@ { - "rust-analyzer.cargo.noDefaultFeatures": true, - "rust-analyzer.cargo.features": ["probe"], + "rust-analyzer.linkedProjects": [ + "acid-firmware/Cargo.toml" + ], + "rust-analyzer.cargo.noDefaultFeatures": true, + "rust-analyzer.cargo.features": ["develop"], + "rust-analyzer.cargo.target": "xtensa-esp32s3-none-elf", + "rust-analyzer.cargo.targetDir": "target/rust-analyzer", + "rust-analyzer.cargo.extraEnv": { + "RUSTUP_TOOLCHAIN": "esp" + }, + "rust-analyzer.check.extraArgs": [ + "-Zbuild-std=core,alloc" + ], + "rust-analyzer.check.extraEnv": { + "EXPLICITLY_INCLUDE_DEFAULT_DIRS": "true", + "XKBCOMMON_BUILD_DIR": "../libxkbcommon/build-esp32s3", + "SPECTRE_API_BUILD_DIR": "../spectre-api-c/build-esp32s3", + "SODIUM_INSTALL_DIR": "../libsodium/install", + "XKBCOMMON_BUILD_DIR_NAME": "build-esp32s3", + "SPECTRE_API_BUILD_DIR_NAME": "build-esp32s3", + "SODIUM_INSTALL_DIR_NAME": "install", + "SLINT_FONT_SIZES": "8,11,10,12,13,14,15,16,18,20,22,24,32" + } } diff --git a/firmware/.zed/settings.json b/firmware/.zed/settings.json index ed6f1f8..883621a 100644 --- a/firmware/.zed/settings.json +++ b/firmware/.zed/settings.json @@ -28,7 +28,7 @@ "extraArgs": ["-Zbuild-std=core,alloc"], // Enable device support and a wide set of features on the esp-rtos crate. "noDefaultFeatures": true, - "features": ["rtt-log"], + "features": ["develop"], }, }, }, diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 82c445f..74faf0e 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "esp-storage", "esp-sync", "gix", + "hex", "hmac", "i-slint-common", "i-slint-core", @@ -3956,6 +3957,7 @@ dependencies = [ "rgb", "scoped-tls-hkt", "scopeguard", + "serde", "skrifa", "slab", "strum", diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index 61da506..62f2027 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" members = ["acid-firmware", "password-hash"] -default-members = [] +default-members = ["acid-firmware"] [workspace.dependencies] spectre-api-sys = { git = "https://github.com/Limeth/spectre-api-sys", rev = "9e844eb056c3dfee8286ac21ec40fa689a8b8aa2" } diff --git a/firmware/acid-firmware/Cargo.toml b/firmware/acid-firmware/Cargo.toml index 9c29018..593d964 100644 --- a/firmware/acid-firmware/Cargo.toml +++ b/firmware/acid-firmware/Cargo.toml @@ -86,10 +86,11 @@ serde_bytes = { version = "0.11.19", default-features = false, features = ["allo chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt tinyvec = { version = "1.10.0", default-features = false, features = ["alloc"] } esp-metadata-generated = { version = "0.3.0", features = ["esp32s3"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } # A fork of slint with patches for `allocator_api` support. # Don't forget to change `slint-build` in build dependencies, if this is changed. -slint = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false, features = ["compat-1-2", "libm", "log", "unsafe-single-threaded", "renderer-software"]} +slint = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false, features = ["compat-1-2", "libm", "log", "unsafe-single-threaded", "renderer-software", "serde"] } i-slint-common = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d" } i-slint-core = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false } diff --git a/firmware/acid-firmware/src/ffi/crypto.rs b/firmware/acid-firmware/src/ffi/crypto.rs index e25401f..5c4452c 100644 --- a/firmware/acid-firmware/src/ffi/crypto.rs +++ b/firmware/acid-firmware/src/ffi/crypto.rs @@ -1,11 +1,14 @@ use core::{ - cell::RefCell, + cell::{Cell, RefCell}, ffi::{c_char, c_int, c_size_t, c_uchar, c_ulonglong}, }; +use critical_section::Mutex; use data_encoding_macro::hexlower; use embassy_sync::blocking_mutex::{self, raw::CriticalSectionRawMutex}; +use esp_sync::RawMutex; use hmac::digest::{FixedOutput, KeyInit, Update}; +use password_hash::Key; use sha2::{ Digest, digest::{consts::U32, generic_array::GenericArray}, @@ -36,6 +39,10 @@ unsafe extern "C" fn __spre_crypto_hash_sha256( 0 } +/// This is the encrypted user key currently being used in the key derivation function of spectre. +/// It decrypts using the user's password into the key that would be derived with the original password hashing function. +pub static ACTIVE_ENCRYPTED_USER_KEY: Mutex> = Mutex::new(Cell::new([0; _])); + #[unsafe(no_mangle)] #[must_use] unsafe extern "C" fn __spre_crypto_pwhash_scryptsalsa208sha256_ll( @@ -51,23 +58,19 @@ unsafe extern "C" fn __spre_crypto_pwhash_scryptsalsa208sha256_ll( ) -> c_int { assert_eq!(output_len, 64); - // TODO: Implement account storage. This is just a key for test:test. - const ENCRYPTED_USER_KEY: [u8; 64] = hexlower!( - "e338e425b4f8d17ac6c349f7ec84087d59a56b6850bcfe950de1af3f04a609d9b1490479086360a38dc209070213c7915e91733a07eced2cec4c6356e050c2be" - ); - - unsafe { + let encryption_key = unsafe { let password: &[u8] = core::slice::from_raw_parts(password, password_len); let salt: &[u8] = core::slice::from_raw_parts(salt, salt_len); - let output: &mut [u8] = core::slice::from_raw_parts_mut(output, output_len); - let purpose = spectre_purpose_scope(SpectreKeyPurpose::Authentication); - let encryption_key = password_hash::derive_encryption_key(salt, password, &PSRAM_ALLOCATOR); - let mut user_key = ENCRYPTED_USER_KEY; - password_hash::decrypt_with(&mut user_key, &encryption_key); - output.copy_from_slice(&user_key); - } + password_hash::derive_encryption_key(salt, password, &PSRAM_ALLOCATOR) + }; + + let output: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(output, output_len) }; + let mut user_key = critical_section::with(|cs| ACTIVE_ENCRYPTED_USER_KEY.borrow(cs).get()); + + password_hash::decrypt_with(&mut user_key, &encryption_key); + output.copy_from_slice(&user_key); 0 } diff --git a/firmware/acid-firmware/src/logging.rs b/firmware/acid-firmware/src/logging.rs index 4ba3187..bd34dec 100644 --- a/firmware/acid-firmware/src/logging.rs +++ b/firmware/acid-firmware/src/logging.rs @@ -72,6 +72,7 @@ pub mod usb { /// Alternative logger via UART. #[cfg(feature = "alt-log")] +#[macro_use] pub mod uart { use super::*; use crate::console; @@ -80,7 +81,6 @@ pub mod uart { use esp_hal::{ Blocking, gpio::interconnect::{PeripheralInput, PeripheralOutput}, - peripherals::UART2, uart::{Uart, UartTx}, }; use log::{Log, info}; @@ -101,10 +101,9 @@ pub mod uart { #[allow(unused)] macro_rules! println { - // TODO: I don't think this is necessary. Consider removing. - // () => {{ - // do_print(Default::default()); - // }}; + () => {{ + do_print(Default::default()); + }}; ($($arg:tt)*) => {{ do_print(::core::format_args!($($arg)*)); @@ -190,7 +189,7 @@ pub mod uart { pub mod rtt { use super::*; #[allow(unused)] - use ::rtt_target::{rprint as print, rprintln as println}; + pub use ::rtt_target::{rprint as print, rprintln as println}; use panic_rtt_target as _; // Use the RTT panic handler. use rtt_target::ChannelMode; @@ -200,3 +199,35 @@ pub mod rtt { async {} } } + +// #[macro_export] +// macro_rules! dbg { +// () => { +// $crate::logging::implementation::println!("[{}:{}:{}]", $crate::file!(), $crate::line!(), $crate::column!()) +// }; +// ($val:expr $(,)?) => { +// match $val { +// tmp => { +// $crate::logging::uart::println!("[{}:{}:{}] {} = {:#?}", +// file!(), +// line!(), +// column!(), +// stringify!($val), +// // The `&T: Debug` check happens here (not in the format literal desugaring) +// // to avoid format literal related messages and suggestions. +// &&tmp as &dyn ::core::fmt::Debug, +// ); +// tmp +// } +// } +// }; +// ($($val:expr),+ $(,)?) => { +// ($($crate::dbg!($val)),+,) +// }; +// } + +// #[cfg(feature = "alt-log")] +// pub use uart as implementation; + +// #[cfg(feature = "rtt-log")] +// pub use rtt as implementation; diff --git a/firmware/acid-firmware/src/main.rs b/firmware/acid-firmware/src/main.rs index a25bf3c..a283fde 100644 --- a/firmware/acid-firmware/src/main.rs +++ b/firmware/acid-firmware/src/main.rs @@ -21,6 +21,8 @@ use core::cell::RefCell; use core::sync::atomic::{AtomicBool, Ordering}; use alloc::boxed::Box; +use alloc::collections::vec_deque::VecDeque; +use alloc::sync::Arc; use alloc::vec; use embassy_embedded_hal::adapter::BlockingAsync; use embassy_embedded_hal::flash::partition::Partition; @@ -97,9 +99,9 @@ esp_bootloader_esp_idf::esp_app_desc!(); // A panic such as `memory allocation of 3740121773 bytes failed` is caused by a heap overflow. The size is `DEEDBAAD` in hex. /// Total heap size -const HEAP_SIZE: usize = 128 * 1024; +const HEAP_SIZE: usize = 112 * 1024; /// Size of the app core's stack -const STACK_SIZE_CORE_APP: usize = 64 * 1024; +const STACK_SIZE_CORE_APP: usize = 80 * 1024; // const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS @@ -392,6 +394,8 @@ async fn main(_spawner: Spawner) { window_size, window: RefCell::new(None), framebuffer: framebuffer_ptr, + quit_event_loop: Default::default(), + events: Arc::new(critical_section::Mutex::new(RefCell::new(VecDeque::new()))), }; spawner.must_spawn(ui::run_renderer_task(slint_backend, flash_part_acid)); }); diff --git a/firmware/acid-firmware/src/ui/backend.rs b/firmware/acid-firmware/src/ui/backend.rs index a3217fb..a78e26f 100644 --- a/firmware/acid-firmware/src/ui/backend.rs +++ b/firmware/acid-firmware/src/ui/backend.rs @@ -1,12 +1,20 @@ -use core::{cell::RefCell, time::Duration}; +use core::{ + cell::RefCell, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; -use alloc::{rc::Rc, string::ToString}; +use alloc::{ + boxed::Box, collections::vec_deque::VecDeque, rc::Rc, string::ToString, sync::Arc, vec::Vec, +}; +use critical_section::Mutex; use esp_hal::time::Instant; use log::{debug, info}; +use rmk::futures::sink::drain; use slint::{ - PhysicalSize, SharedString, WindowSize, + EventLoopError, PhysicalSize, SharedString, WindowSize, platform::{ - Key, WindowEvent, + EventLoopProxy, Key, WindowEvent, software_renderer::{RenderingRotation, RepaintBufferType, Rgb565Pixel, SoftwareRenderer}, }, }; @@ -24,7 +32,8 @@ pub struct SlintBackend { pub window_size: [u32; 2], pub window: RefCell>>, pub framebuffer: FramebufferPtr, - // pub peripherals: RefCell>, + pub quit_event_loop: Arc, + pub events: Arc>>>>, } impl slint::platform::Platform for SlintBackend { @@ -49,12 +58,31 @@ impl slint::platform::Platform for SlintBackend { Duration::from_millis(Instant::now().duration_since_epoch().as_millis()) } + fn new_event_loop_proxy(&self) -> Option> { + Some(Box::new(AcidEventLoopProxy { + quit_event_loop: self.quit_event_loop.clone(), + events: self.events.clone(), + })) + } + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { // Instead of `loop`ing here, we execute a single iteration and handle `loop`ing // in `crate::run_renderer_task`, where we can make use of `await`. /* loop */ { + let drained_events = critical_section::with(|cs| { + self.events + .borrow(cs) + .borrow_mut() + .drain(..) + .collect::>() + }); + + for event in drained_events { + (event)(); + } + if let Some(window) = self.window.borrow().clone() { // Handle key presses while let Ok(mut key_message) = KEY_MESSAGE_CHANNEL.try_receive() { @@ -62,28 +90,28 @@ impl slint::platform::Platform for SlintBackend { if let Some(string) = key_message.string.as_ref() && (Keysym::a..=Keysym::z).contains(&key_message.keysym) - && let &[code] = string.as_bytes() { - const UNICODE_CTRL_A: char = '\u{1}'; + && let &[code] = string.as_bytes() + { + const UNICODE_CTRL_A: char = '\u{1}'; - let letter_index_from_keysym = - key_message.keysym.raw().wrapping_sub(Keysym::a.raw()); - let letter_index_from_string = - (code as u32).wrapping_sub(UNICODE_CTRL_A as u32); + let letter_index_from_keysym = + key_message.keysym.raw().wrapping_sub(Keysym::a.raw()); + let letter_index_from_string = + (code as u32).wrapping_sub(UNICODE_CTRL_A as u32); - if letter_index_from_keysym == letter_index_from_string { - key_message.keysym = - Keysym::new(Keysym::a.raw() + letter_index_from_keysym); - // TODO: Avoid allocation - key_message.string = Some( - ((b'a' + letter_index_from_keysym as u8) as char).to_string(), - ); + if letter_index_from_keysym == letter_index_from_string { + key_message.keysym = + Keysym::new(Keysym::a.raw() + letter_index_from_keysym); + // TODO: Avoid allocation + key_message.string = + Some(((b'a' + letter_index_from_keysym as u8) as char).to_string()); - info!( - "Translating CTRL-{letter} to {letter}", - letter = (b'A' + letter_index_from_keysym as u8) as char - ); - } + info!( + "Translating CTRL-{letter} to {letter}", + letter = (b'A' + letter_index_from_keysym as u8) as char + ); } + } // let text = key_message.string.map(SharedString::from).or_else(|| { // Key::try_from_keysym(key_message.keysym).map(SharedString::from) @@ -118,3 +146,25 @@ impl slint::platform::Platform for SlintBackend { Ok(()) } } + +struct AcidEventLoopProxy { + pub quit_event_loop: Arc, + pub events: Arc>>>>, +} + +impl EventLoopProxy for AcidEventLoopProxy { + fn quit_event_loop(&self) -> Result<(), EventLoopError> { + self.quit_event_loop.store(true, Ordering::SeqCst); + Ok(()) + } + + fn invoke_from_event_loop( + &self, + event: Box, + ) -> Result<(), EventLoopError> { + critical_section::with(|cs| { + self.events.borrow(cs).borrow_mut().push_back(event); + }); + Ok(()) + } +} diff --git a/firmware/acid-firmware/src/ui/messages.rs b/firmware/acid-firmware/src/ui/messages.rs index 018cd28..a032ca8 100644 --- a/firmware/acid-firmware/src/ui/messages.rs +++ b/firmware/acid-firmware/src/ui/messages.rs @@ -21,13 +21,10 @@ pub enum CallbackMessageUsers { } pub enum CallbackMessageUserEdit { - ComputeIdenticon { - encrypted_key: SharedString, - password: SharedString, - }, - Confirm { - encrypted_key: SharedString, - }, + ComputeIdenticon { password: SharedString }, + ComputeKeyId { key: SharedString }, + ConfirmRequest { encrypted_key: SharedString }, + ConfirmProcessed, } pub enum CallbackMessageUserSites {} diff --git a/firmware/acid-firmware/src/ui/mod.rs b/firmware/acid-firmware/src/ui/mod.rs index 64b82b9..f222e5f 100644 --- a/firmware/acid-firmware/src/ui/mod.rs +++ b/firmware/acid-firmware/src/ui/mod.rs @@ -1,25 +1,27 @@ // #![cfg_attr(not(feature = "simulator"), no_main)] -use core::{cell::RefCell, ffi::CStr}; +use core::{cell::RefCell, ffi::CStr, ops::DerefMut}; use alloc::{boxed::Box, ffi::CString, format, rc::Rc, vec}; use embassy_time::Instant; +use hex::FromHexError; use log::{info, warn}; +use password_hash::Key; use slint::SharedString; -use spectre_api_sys::{SpectreAlgorithm, SpectreUserKey}; +use spectre_api_sys::{SpectreAlgorithm, SpectreKeyID, 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}, - ffi::alloc::__spre_free, + ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY}, ui::{ backend::SlintBackend, messages::{ CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers, }, - storage::SpectreUsersConfig, + storage::{SpectreUserConfig, SpectreUsersConfig}, }, util::DurationExt, }; @@ -155,13 +157,14 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition let main = AppWindow::new().unwrap(); let state = State::new(db, main).await; + let window = state.borrow().window.clone_strong(); - state.borrow().run_event_loop().await; + State::run_event_loop(window).await; } struct State { window: AppWindow, - db: AcidDatabase, + db: Rc, users: SpectreUsersConfig, /// Currently active view. view: AppState, @@ -177,18 +180,11 @@ impl State { let state = Rc::new(RefCell::new(State { window: main.clone_strong(), users: { - 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:?}"), - } + let users = Self::load_users(&db).await; + warn!("Users: {users:#?}"); + users }, - db, + db: Rc::new(db), view: AppState::Login, state_login: Default::default(), state_users: Default::default(), @@ -206,56 +202,61 @@ impl State { main.on_escape({ let state = state.clone(); move || { - state - .borrow_mut() - .process_callback_message(CallbackMessage::Escape); + State::process_callback_message(&state, CallbackMessage::Escape); } }); main.on_login_pw_accepted({ let state = state.clone(); move |username, password| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::Login( - CallbackMessageLogin::PwAccepted { username, password }, - )); + State::process_callback_message( + &state, + CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }), + ); } }); main.on_users_edit_user({ let state = state.clone(); move |username, new| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::Users( - CallbackMessageUsers::EditUser { username, new }, - )); + State::process_callback_message( + &state, + CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }), + ); } }); main.on_user_edit_compute_identicon({ let state = state.clone(); - move |encrypted_key, password| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::UserEdit( - CallbackMessageUserEdit::ComputeIdenticon { - encrypted_key, - password, - }, - )); + 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 - .borrow_mut() - .process_callback_message(CallbackMessage::UserEdit( - CallbackMessageUserEdit::Confirm { encrypted_key }, - )); + State::process_callback_message( + &state, + CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest { + encrypted_key, + }), + ); } }); @@ -272,12 +273,36 @@ impl State { state } - fn process_callback_message(&mut self, message: CallbackMessage) { - match self.view { - AppState::Login => StateLogin::process_callback_message(self, message), - AppState::Users => StateUsers::process_callback_message(self, message), - AppState::UserEdit => StateUserEdit::process_callback_message(self, message), - AppState::UserSites => StateUserSites::process_callback_message(self, message), + 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 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), } } @@ -298,8 +323,8 @@ impl State { /// 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(&self) -> ! { - self.window.show().unwrap(); + async fn run_event_loop(window: AppWindow) -> ! { + window.show().unwrap(); loop { slint::run_event_loop().unwrap(); @@ -310,19 +335,20 @@ impl State { } #[expect(unreachable_code)] - self.window.hide().unwrap(); + window.hide().unwrap(); } } trait AppViewTrait { - fn process_callback_message(_state: &mut State, _message: CallbackMessage) {} + fn process_callback_message(_state_rc: &Rc>, _message: CallbackMessage) {} } #[derive(Default)] struct StateLogin {} impl AppViewTrait for StateLogin { - fn process_callback_message(state: &mut State, message: CallbackMessage) { + fn process_callback_message(state_rc: &Rc>, message: CallbackMessage) { + let mut state = state_rc.borrow_mut(); if let CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) = message { @@ -372,7 +398,8 @@ impl AppViewTrait for StateLogin { struct StateUsers {} impl AppViewTrait for StateUsers { - fn process_callback_message(state: &mut State, message: CallbackMessage) { + 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, false); @@ -381,7 +408,8 @@ impl AppViewTrait for StateUsers { state.state_user_edit = StateUserEdit { username: username.clone(), new, - hashed: None, + password: None, + encrypted_key: None, }; state.window.set_user_edit_username(username); state.set_view(AppState::UserEdit, false); @@ -395,19 +423,19 @@ impl AppViewTrait for StateUsers { struct StateUserEdit { username: SharedString, new: bool, - hashed: Option, + password: Option, + encrypted_key: Option<(Key, SpectreUserKey)>, } impl AppViewTrait for StateUserEdit { - fn process_callback_message(state: &mut State, message: CallbackMessage) { + 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, false); } - CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { - encrypted_key, - password, - }) => { + 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 = @@ -431,24 +459,107 @@ impl AppViewTrait for StateUserEdit { warn!("Identicon: {identicon} ({identicon:?})"); state.window.set_user_edit_identicon(identicon.clone()); - state.state_user_edit.hashed = Some(ProposedPassword { - encrypted_key, - identicon, - }) + state.state_user_edit.password = Some(password); } - CallbackMessage::UserEdit(CallbackMessageUserEdit::Confirm { encrypted_key: _ }) => { - // TODO + 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; + } + + critical_section::with(|cs| { + ACTIVE_ENCRYPTED_USER_KEY.borrow(cs).set(key); + }); + 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); + + 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 { + encrypted_key: _, + }) => { + 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 username = state.state_user_edit.username.clone(); + state + .users + .users + .retain(|user| &*user.username != &*username); + state.users.users.push(SpectreUserConfig { + username, + encrypted_key, + key_id, + }); + 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); } _ => (), } } } -struct ProposedPassword { - encrypted_key: SharedString, - identicon: SharedString, -} - struct StateUserSites { username: SharedString, user_key: SpectreUserKey, diff --git a/firmware/acid-firmware/src/ui/storage.rs b/firmware/acid-firmware/src/ui/storage.rs index 958b044..3243b7e 100644 --- a/firmware/acid-firmware/src/ui/storage.rs +++ b/firmware/acid-firmware/src/ui/storage.rs @@ -2,6 +2,7 @@ use alloc::{string::String, vec::Vec}; use chrono::NaiveDateTime; use password_hash::Key; use serde::{Deserialize, Serialize}; +use slint::SharedString; use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType}; #[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)] @@ -11,7 +12,7 @@ pub struct SpectreUsersConfig { #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] pub struct SpectreUserConfig { - pub username: String, + pub username: SharedString, #[serde(with = "serde_bytes")] pub encrypted_key: Key, #[serde(with = "serde_bytes")] @@ -26,10 +27,10 @@ pub struct SpectreSiteConfig { #[serde(rename = "type")] #[serde(with = "with_repr")] pub result_type: SpectreResultType, - pub password: Option, + pub password: Option, #[serde(with = "with_repr")] pub login_type: SpectreResultType, - pub login_name: Option, + pub login_name: Option, pub uses: u32, pub last_used: NaiveDateTime, } @@ -51,8 +52,8 @@ impl Default for SpectreSiteConfig { #[derive(Clone, Debug, PartialEq, Eq)] pub struct SpectreSite { - pub username: String, - pub site_name: String, + pub username: SharedString, + pub site_name: SharedString, pub config: SpectreSiteConfig, } diff --git a/firmware/acid-firmware/ui/main.slint b/firmware/acid-firmware/ui/main.slint index d0ba5bc..7ee677a 100644 --- a/firmware/acid-firmware/ui/main.slint +++ b/firmware/acid-firmware/ui/main.slint @@ -43,8 +43,11 @@ export component AppWindow inherits Window { callback users_edit_user <=> users_view.edit_user; // User Edit View in property user_edit_username <=> user_edit_view.username; + in property user_edit_key_error <=> user_edit_view.key_error; in-out property user_edit_identicon <=> user_edit_view.identicon; + in-out property user_edit_key_id <=> user_edit_view.key_id; callback user_edit_compute_identicon <=> user_edit_view.compute_identicon; + 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; diff --git a/firmware/acid-firmware/ui/user-edit-view.slint b/firmware/acid-firmware/ui/user-edit-view.slint index 3d841de..67b6817 100644 --- a/firmware/acid-firmware/ui/user-edit-view.slint +++ b/firmware/acid-firmware/ui/user-edit-view.slint @@ -6,8 +6,11 @@ export component UserEditView inherits HorizontalLayout { padding: Style.spacing; spacing: Style.spacing; in property username; + in property key_error; in-out property identicon; - callback compute_identicon(encrypted_key: string, password: string); + in-out property key_id; + callback compute_identicon(password: string); + callback compute_key_id(encrypted_key: string); callback confirm(encrypted_key: string); callback cancel <=> button_cancel.clicked; VerticalLayout { @@ -18,25 +21,16 @@ export component UserEditView inherits HorizontalLayout { text: "Enter " + username + "'s encrypted key and press Enter:"; } - line_edit_encrypted_key := LineEdit { - input-type: InputType.text; - placeholder-text: "Encrypted key"; - edited(text) => { - root.identicon = ""; - } - accepted(text) => { - line_edit_password.focus(); - } - } - line_edit_password := LineEdit { - input-type: InputType.text; + input-type: InputType.password; placeholder-text: "Password"; edited(text) => { root.identicon = ""; + root.key_id = ""; } accepted(text) => { - compute_identicon(line_edit_encrypted_key.text, text); + compute_identicon(text); + line_edit_password.focus(); } } @@ -44,6 +38,23 @@ export component UserEditView inherits HorizontalLayout { text: identicon.is-empty ? "" : ("Check the identicon: " + identicon); } + line_edit_encrypted_key := LineEdit { + input-type: InputType.text; + placeholder-text: "Encrypted key"; + enabled: !identicon.is-empty; + edited(text) => { + root.key_id = ""; + } + accepted(text) => { + compute_key_id(text); + button_confirm.focus(); + } + } + + Text { + text: !key_error.is-empty ? key_error : (!key_id.is-empty ? ("Key ID: " + key_id) : ""); + } + HorizontalLayout { spacing: Style.spacing; button_cancel := Button { @@ -52,7 +63,7 @@ export component UserEditView inherits HorizontalLayout { button_confirm := Button { text: "Confirm"; - enabled: !identicon.is-empty; + enabled: !identicon.is-empty && !key_id.is-empty; clicked => { confirm(line_edit_encrypted_key.text); }