acid/firmware/acid-firmware/src/ui/mod.rs

569 lines
20 KiB
Rust
Raw Normal View History

// #![cfg_attr(not(feature = "simulator"), no_main)]
2026-02-04 03:14:21 +01:00
use core::{cell::RefCell, ffi::CStr, ops::DerefMut};
2026-01-24 00:42:16 +01:00
2026-01-31 15:36:36 +01:00
use alloc::{boxed::Box, ffi::CString, format, rc::Rc, vec};
use embassy_time::Instant;
2026-02-04 03:14:21 +01:00
use hex::FromHexError;
2026-01-20 02:55:17 +01:00
use log::{info, warn};
2026-02-04 03:14:21 +01:00
use password_hash::Key;
2026-01-31 15:36:36 +01:00
use slint::SharedString;
2026-02-04 03:14:21 +01:00
use spectre_api_sys::{SpectreAlgorithm, SpectreKeyID, SpectreUserKey};
2026-01-10 19:21:13 +01:00
#[cfg(feature = "limit-fps")]
use crate::FRAME_DURATION_MIN;
2026-01-24 21:12:25 +01:00
use crate::{
PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
2026-01-31 15:36:36 +01:00
db::{AcidDatabase, DbKey, DbPathSpectreUsers, PartitionAcid, ReadTransactionExt},
2026-02-04 03:14:21 +01:00
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
2026-01-31 15:24:36 +01:00
ui::{
backend::SlintBackend,
messages::{
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers,
},
2026-02-04 03:14:21 +01:00
storage::{SpectreUserConfig, SpectreUsersConfig},
2026-01-31 15:24:36 +01:00
},
2026-01-24 21:12:25 +01:00
util::DurationExt,
};
2026-01-10 19:21:13 +01:00
2025-12-31 22:24:26 +01:00
pub mod backend;
2026-01-31 15:24:36 +01:00
pub mod messages;
pub mod storage;
2026-01-10 19:21:13 +01:00
pub mod window_adapter;
2025-12-31 22:24:26 +01:00
slint::include_modules!();
2026-01-10 19:21:13 +01:00
2026-01-27 02:46:53 +01:00
fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey {
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()
);
2026-01-31 15:36:36 +01:00
let user_key_stack = *user_key;
2026-01-27 02:46:53 +01:00
// TODO: Erase memory before freeing
__spre_free(user_key as *const _ as *mut _);
user_key_stack
}
}
2026-01-10 19:21:13 +01:00
#[embassy_executor::task]
2026-01-24 00:42:16 +01:00
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
2026-01-24 21:12:25 +01:00
let db = AcidDatabase::mount(flash_part_acid).await;
2026-01-25 01:45:25 +01:00
// 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::<SpectreUsersConfig>(&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::<SpectreSiteConfig>(&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");
2026-01-24 00:42:16 +01:00
2026-01-24 21:12:25 +01:00
// TODO:
// * Store a config as a versioned postcard-serialized struct
// * Store accounts and sites as ranges in the DB
2026-01-24 00:42:16 +01:00
i_slint_core::properties::ALLOCATOR
.set(&PSRAM_ALLOCATOR)
.ok()
.unwrap();
2026-01-10 19:21:13 +01:00
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
2026-01-31 15:24:36 +01:00
let main = AppWindow::new().unwrap();
let state = State::new(db, main).await;
2026-02-04 03:14:21 +01:00
let window = state.borrow().window.clone_strong();
2026-01-27 02:46:53 +01:00
2026-02-04 03:14:21 +01:00
State::run_event_loop(window).await;
2026-01-31 15:24:36 +01:00
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
struct State {
window: AppWindow,
2026-02-04 03:14:21 +01:00
db: Rc<AcidDatabase>,
2026-01-31 15:24:36 +01:00
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<StateUserSites>,
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
impl State {
async fn new(db: AcidDatabase, main: AppWindow) -> Rc<RefCell<Self>> {
let state = Rc::new(RefCell::new(State {
window: main.clone_strong(),
users: {
2026-02-04 03:14:21 +01:00
let users = Self::load_users(&db).await;
warn!("Users: {users:#?}");
users
2026-01-31 15:24:36 +01:00
},
2026-02-04 03:14:21 +01:00
db: Rc::new(db),
2026-01-31 15:24:36 +01:00
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);
2026-01-27 02:46:53 +01:00
}
2026-01-31 15:24:36 +01:00
});
main.on_escape({
let state = state.clone();
move || {
2026-02-04 03:14:21 +01:00
State::process_callback_message(&state, CallbackMessage::Escape);
2026-01-27 02:46:53 +01:00
}
2026-01-31 15:24:36 +01:00
});
main.on_login_pw_accepted({
let state = state.clone();
move |username, password| {
2026-02-04 03:14:21 +01:00
State::process_callback_message(
&state,
CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }),
);
2026-01-31 15:24:36 +01:00
}
});
main.on_users_edit_user({
let state = state.clone();
move |username, new| {
2026-02-04 03:14:21 +01:00
State::process_callback_message(
&state,
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }),
);
2026-01-31 15:24:36 +01:00
}
});
main.on_user_edit_compute_identicon({
let state = state.clone();
2026-02-04 03:14:21 +01:00
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 }),
);
2026-01-31 15:24:36 +01:00
}
});
main.on_user_edit_confirm({
let state = state.clone();
move |encrypted_key| {
2026-02-04 03:14:21 +01:00
State::process_callback_message(
&state,
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest {
encrypted_key,
}),
);
2026-01-31 15:24:36 +01:00
}
});
// 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
2026-01-27 02:46:53 +01:00
}
2026-02-04 03:14:21 +01:00
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::<SpectreUsersConfig>(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<RefCell<State>>, 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),
2026-01-27 02:46:53 +01:00
}
}
2026-01-31 15:24:36 +01:00
fn set_view(&mut self, view: AppState, reset: bool) {
self.view = view;
self.window.set_app_state(view);
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
if reset {
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(),
2026-01-27 02:46:53 +01:00
}
}
}
2026-01-31 15:24:36 +01:00
/// 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()`.
2026-02-04 03:14:21 +01:00
async fn run_event_loop(window: AppWindow) -> ! {
window.show().unwrap();
2026-01-31 15:24:36 +01:00
loop {
slint::run_event_loop().unwrap();
SIGNAL_LCD_SUBMIT.signal(());
#[cfg(feature = "limit-fps")]
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
SIGNAL_UI_RENDER.wait().await;
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
#[expect(unreachable_code)]
2026-02-04 03:14:21 +01:00
window.hide().unwrap();
2026-01-27 02:46:53 +01:00
}
2026-01-31 15:24:36 +01:00
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
trait AppViewTrait {
2026-02-04 03:14:21 +01:00
fn process_callback_message(_state_rc: &Rc<RefCell<State>>, _message: CallbackMessage) {}
2026-01-31 15:24:36 +01:00
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
#[derive(Default)]
struct StateLogin {}
impl AppViewTrait for StateLogin {
2026-02-04 03:14:21 +01:00
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
let mut state = state_rc.borrow_mut();
2026-01-31 15:36:36 +01:00
if let CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) =
message
{
let username_c =
CString::new(&*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 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 Some(user) = state
.users
.users
.iter()
.find(|user| *user.username == *username)
else {
return;
};
if user.key_id == user_key.keyID.bytes {
info!("Correct password entered for user {username:?}.");
state.state_user_sites = Some(StateUserSites { username, user_key });
state.set_view(AppState::UserSites, true);
} else {
warn!("Incorrect password entered for user {username:?}.");
// TODO: Clear the input
2026-01-31 15:24:36 +01:00
}
}
2026-01-27 02:46:53 +01:00
}
2026-01-31 15:24:36 +01:00
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
#[derive(Default)]
struct StateUsers {}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
impl AppViewTrait for StateUsers {
2026-02-04 03:14:21 +01:00
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
let mut state = state_rc.borrow_mut();
2026-01-31 15:24:36 +01:00
match message {
CallbackMessage::Escape => {
state.set_view(AppState::Login, false);
}
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => {
state.state_user_edit = StateUserEdit {
username: username.clone(),
new,
2026-02-04 03:14:21 +01:00
password: None,
encrypted_key: None,
2026-01-31 15:24:36 +01:00
};
state.window.set_user_edit_username(username);
state.set_view(AppState::UserEdit, false);
}
_ => (),
}
2026-01-27 02:46:53 +01:00
}
2026-01-31 15:24:36 +01:00
}
2026-01-27 02:46:53 +01:00
2026-01-31 15:24:36 +01:00
#[derive(Default)]
struct StateUserEdit {
username: SharedString,
new: bool,
2026-02-04 03:14:21 +01:00
password: Option<SharedString>,
encrypted_key: Option<(Key, SpectreUserKey)>,
2026-01-31 15:24:36 +01:00
}
2026-01-10 19:21:13 +01:00
2026-01-31 15:24:36 +01:00
impl AppViewTrait for StateUserEdit {
2026-02-04 03:14:21 +01:00
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
let state = state_rc.clone();
let mut state = state.borrow_mut();
2026-01-31 15:24:36 +01:00
match message {
CallbackMessage::Escape => {
state.set_view(AppState::Users, false);
2026-01-27 02:46:53 +01:00
}
2026-02-04 03:14:21 +01:00
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password }) => {
2026-01-31 15:24:36 +01:00
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());
2026-02-04 03:14:21 +01:00
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;
}
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,
2026-01-31 15:24:36 +01:00
encrypted_key,
2026-02-04 03:14:21 +01:00
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),
);
}
2026-01-31 15:24:36 +01:00
})
2026-02-04 03:14:21 +01:00
.unwrap();
2026-01-31 15:24:36 +01:00
}
2026-02-04 03:14:21 +01:00
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed) => {
state.state_user_edit = Default::default();
state.set_view(AppState::Users, true);
2026-01-31 15:24:36 +01:00
}
_ => (),
}
2026-01-31 15:24:36 +01:00
}
2026-01-24 00:42:16 +01:00
}
2026-01-10 19:21:13 +01:00
2026-01-31 15:24:36 +01:00
struct StateUserSites {
username: SharedString,
user_key: SpectreUserKey,
2026-01-10 19:21:13 +01:00
}
2026-01-31 15:24:36 +01:00
impl AppViewTrait for StateUserSites {}