Implement spectre user addition

This commit is contained in:
Jakub Hlusička 2026-02-04 03:14:21 +01:00
parent 3ac1656d33
commit 40b9b5d278
14 changed files with 386 additions and 151 deletions

View file

@ -1,4 +1,25 @@
{ {
"rust-analyzer.linkedProjects": [
"acid-firmware/Cargo.toml"
],
"rust-analyzer.cargo.noDefaultFeatures": true, "rust-analyzer.cargo.noDefaultFeatures": true,
"rust-analyzer.cargo.features": ["probe"], "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"
}
} }

View file

@ -28,7 +28,7 @@
"extraArgs": ["-Zbuild-std=core,alloc"], "extraArgs": ["-Zbuild-std=core,alloc"],
// Enable device support and a wide set of features on the esp-rtos crate. // Enable device support and a wide set of features on the esp-rtos crate.
"noDefaultFeatures": true, "noDefaultFeatures": true,
"features": ["rtt-log"], "features": ["develop"],
}, },
}, },
}, },

2
firmware/Cargo.lock generated
View file

@ -37,6 +37,7 @@ dependencies = [
"esp-storage", "esp-storage",
"esp-sync", "esp-sync",
"gix", "gix",
"hex",
"hmac", "hmac",
"i-slint-common", "i-slint-common",
"i-slint-core", "i-slint-core",
@ -3956,6 +3957,7 @@ dependencies = [
"rgb", "rgb",
"scoped-tls-hkt", "scoped-tls-hkt",
"scopeguard", "scopeguard",
"serde",
"skrifa", "skrifa",
"slab", "slab",
"strum", "strum",

View file

@ -1,7 +1,7 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["acid-firmware", "password-hash"] members = ["acid-firmware", "password-hash"]
default-members = [] default-members = ["acid-firmware"]
[workspace.dependencies] [workspace.dependencies]
spectre-api-sys = { git = "https://github.com/Limeth/spectre-api-sys", rev = "9e844eb056c3dfee8286ac21ec40fa689a8b8aa2" } spectre-api-sys = { git = "https://github.com/Limeth/spectre-api-sys", rev = "9e844eb056c3dfee8286ac21ec40fa689a8b8aa2" }

View file

@ -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 chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt
tinyvec = { version = "1.10.0", default-features = false, features = ["alloc"] } tinyvec = { version = "1.10.0", default-features = false, features = ["alloc"] }
esp-metadata-generated = { version = "0.3.0", features = ["esp32s3"] } 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. # A fork of slint with patches for `allocator_api` support.
# Don't forget to change `slint-build` in build dependencies, if this is changed. # 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-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 } i-slint-core = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false }

View file

@ -1,11 +1,14 @@
use core::{ use core::{
cell::RefCell, cell::{Cell, RefCell},
ffi::{c_char, c_int, c_size_t, c_uchar, c_ulonglong}, ffi::{c_char, c_int, c_size_t, c_uchar, c_ulonglong},
}; };
use critical_section::Mutex;
use data_encoding_macro::hexlower; use data_encoding_macro::hexlower;
use embassy_sync::blocking_mutex::{self, raw::CriticalSectionRawMutex}; use embassy_sync::blocking_mutex::{self, raw::CriticalSectionRawMutex};
use esp_sync::RawMutex;
use hmac::digest::{FixedOutput, KeyInit, Update}; use hmac::digest::{FixedOutput, KeyInit, Update};
use password_hash::Key;
use sha2::{ use sha2::{
Digest, Digest,
digest::{consts::U32, generic_array::GenericArray}, digest::{consts::U32, generic_array::GenericArray},
@ -36,6 +39,10 @@ unsafe extern "C" fn __spre_crypto_hash_sha256(
0 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<Cell<Key>> = Mutex::new(Cell::new([0; _]));
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
#[must_use] #[must_use]
unsafe extern "C" fn __spre_crypto_pwhash_scryptsalsa208sha256_ll( unsafe extern "C" fn __spre_crypto_pwhash_scryptsalsa208sha256_ll(
@ -51,23 +58,19 @@ unsafe extern "C" fn __spre_crypto_pwhash_scryptsalsa208sha256_ll(
) -> c_int { ) -> c_int {
assert_eq!(output_len, 64); assert_eq!(output_len, 64);
// TODO: Implement account storage. This is just a key for test:test. let encryption_key = unsafe {
const ENCRYPTED_USER_KEY: [u8; 64] = hexlower!(
"e338e425b4f8d17ac6c349f7ec84087d59a56b6850bcfe950de1af3f04a609d9b1490479086360a38dc209070213c7915e91733a07eced2cec4c6356e050c2be"
);
unsafe {
let password: &[u8] = core::slice::from_raw_parts(password, password_len); let password: &[u8] = core::slice::from_raw_parts(password, password_len);
let salt: &[u8] = core::slice::from_raw_parts(salt, salt_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 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::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); password_hash::decrypt_with(&mut user_key, &encryption_key);
output.copy_from_slice(&user_key); output.copy_from_slice(&user_key);
}
0 0
} }

View file

@ -72,6 +72,7 @@ pub mod usb {
/// Alternative logger via UART. /// Alternative logger via UART.
#[cfg(feature = "alt-log")] #[cfg(feature = "alt-log")]
#[macro_use]
pub mod uart { pub mod uart {
use super::*; use super::*;
use crate::console; use crate::console;
@ -80,7 +81,6 @@ pub mod uart {
use esp_hal::{ use esp_hal::{
Blocking, Blocking,
gpio::interconnect::{PeripheralInput, PeripheralOutput}, gpio::interconnect::{PeripheralInput, PeripheralOutput},
peripherals::UART2,
uart::{Uart, UartTx}, uart::{Uart, UartTx},
}; };
use log::{Log, info}; use log::{Log, info};
@ -101,10 +101,9 @@ pub mod uart {
#[allow(unused)] #[allow(unused)]
macro_rules! println { macro_rules! println {
// TODO: I don't think this is necessary. Consider removing. () => {{
// () => {{ do_print(Default::default());
// do_print(Default::default()); }};
// }};
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
do_print(::core::format_args!($($arg)*)); do_print(::core::format_args!($($arg)*));
@ -190,7 +189,7 @@ pub mod uart {
pub mod rtt { pub mod rtt {
use super::*; use super::*;
#[allow(unused)] #[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 panic_rtt_target as _; // Use the RTT panic handler.
use rtt_target::ChannelMode; use rtt_target::ChannelMode;
@ -200,3 +199,35 @@ pub mod rtt {
async {} 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;

View file

@ -21,6 +21,8 @@ use core::cell::RefCell;
use core::sync::atomic::{AtomicBool, Ordering}; use core::sync::atomic::{AtomicBool, Ordering};
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::collections::vec_deque::VecDeque;
use alloc::sync::Arc;
use alloc::vec; use alloc::vec;
use embassy_embedded_hal::adapter::BlockingAsync; use embassy_embedded_hal::adapter::BlockingAsync;
use embassy_embedded_hal::flash::partition::Partition; 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. // 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 /// Total heap size
const HEAP_SIZE: usize = 128 * 1024; const HEAP_SIZE: usize = 112 * 1024;
/// Size of the app core's stack /// 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(40); // 25 FPS
const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS
@ -392,6 +394,8 @@ async fn main(_spawner: Spawner) {
window_size, window_size,
window: RefCell::new(None), window: RefCell::new(None),
framebuffer: framebuffer_ptr, 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)); spawner.must_spawn(ui::run_renderer_task(slint_backend, flash_part_acid));
}); });

View file

@ -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 esp_hal::time::Instant;
use log::{debug, info}; use log::{debug, info};
use rmk::futures::sink::drain;
use slint::{ use slint::{
PhysicalSize, SharedString, WindowSize, EventLoopError, PhysicalSize, SharedString, WindowSize,
platform::{ platform::{
Key, WindowEvent, EventLoopProxy, Key, WindowEvent,
software_renderer::{RenderingRotation, RepaintBufferType, Rgb565Pixel, SoftwareRenderer}, software_renderer::{RenderingRotation, RepaintBufferType, Rgb565Pixel, SoftwareRenderer},
}, },
}; };
@ -24,7 +32,8 @@ pub struct SlintBackend {
pub window_size: [u32; 2], pub window_size: [u32; 2],
pub window: RefCell<Option<Rc<SoftwareWindowAdapter>>>, pub window: RefCell<Option<Rc<SoftwareWindowAdapter>>>,
pub framebuffer: FramebufferPtr, pub framebuffer: FramebufferPtr,
// pub peripherals: RefCell<Option<Peripherals>>, pub quit_event_loop: Arc<AtomicBool>,
pub events: Arc<Mutex<RefCell<VecDeque<Box<dyn FnOnce() + Send>>>>>,
} }
impl slint::platform::Platform for SlintBackend { 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()) Duration::from_millis(Instant::now().duration_since_epoch().as_millis())
} }
fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
Some(Box::new(AcidEventLoopProxy {
quit_event_loop: self.quit_event_loop.clone(),
events: self.events.clone(),
}))
}
fn run_event_loop(&self) -> Result<(), slint::PlatformError> { fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
// Instead of `loop`ing here, we execute a single iteration and handle `loop`ing // 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`. // in `crate::run_renderer_task`, where we can make use of `await`.
/* loop */ /* loop */
{ {
let drained_events = critical_section::with(|cs| {
self.events
.borrow(cs)
.borrow_mut()
.drain(..)
.collect::<Vec<_>>()
});
for event in drained_events {
(event)();
}
if let Some(window) = self.window.borrow().clone() { if let Some(window) = self.window.borrow().clone() {
// Handle key presses // Handle key presses
while let Ok(mut key_message) = KEY_MESSAGE_CHANNEL.try_receive() { while let Ok(mut key_message) = KEY_MESSAGE_CHANNEL.try_receive() {
@ -62,7 +90,8 @@ impl slint::platform::Platform for SlintBackend {
if let Some(string) = key_message.string.as_ref() if let Some(string) = key_message.string.as_ref()
&& (Keysym::a..=Keysym::z).contains(&key_message.keysym) && (Keysym::a..=Keysym::z).contains(&key_message.keysym)
&& let &[code] = string.as_bytes() { && let &[code] = string.as_bytes()
{
const UNICODE_CTRL_A: char = '\u{1}'; const UNICODE_CTRL_A: char = '\u{1}';
let letter_index_from_keysym = let letter_index_from_keysym =
@ -74,9 +103,8 @@ impl slint::platform::Platform for SlintBackend {
key_message.keysym = key_message.keysym =
Keysym::new(Keysym::a.raw() + letter_index_from_keysym); Keysym::new(Keysym::a.raw() + letter_index_from_keysym);
// TODO: Avoid allocation // TODO: Avoid allocation
key_message.string = Some( key_message.string =
((b'a' + letter_index_from_keysym as u8) as char).to_string(), Some(((b'a' + letter_index_from_keysym as u8) as char).to_string());
);
info!( info!(
"Translating CTRL-{letter} to {letter}", "Translating CTRL-{letter} to {letter}",
@ -118,3 +146,25 @@ impl slint::platform::Platform for SlintBackend {
Ok(()) Ok(())
} }
} }
struct AcidEventLoopProxy {
pub quit_event_loop: Arc<AtomicBool>,
pub events: Arc<Mutex<RefCell<VecDeque<Box<dyn FnOnce() + Send>>>>>,
}
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<dyn FnOnce() + Send>,
) -> Result<(), EventLoopError> {
critical_section::with(|cs| {
self.events.borrow(cs).borrow_mut().push_back(event);
});
Ok(())
}
}

View file

@ -21,13 +21,10 @@ pub enum CallbackMessageUsers {
} }
pub enum CallbackMessageUserEdit { pub enum CallbackMessageUserEdit {
ComputeIdenticon { ComputeIdenticon { password: SharedString },
encrypted_key: SharedString, ComputeKeyId { key: SharedString },
password: SharedString, ConfirmRequest { encrypted_key: SharedString },
}, ConfirmProcessed,
Confirm {
encrypted_key: SharedString,
},
} }
pub enum CallbackMessageUserSites {} pub enum CallbackMessageUserSites {}

View file

@ -1,25 +1,27 @@
// #![cfg_attr(not(feature = "simulator"), no_main)] // #![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 alloc::{boxed::Box, ffi::CString, format, rc::Rc, vec};
use embassy_time::Instant; use embassy_time::Instant;
use hex::FromHexError;
use log::{info, warn}; use log::{info, warn};
use password_hash::Key;
use slint::SharedString; use slint::SharedString;
use spectre_api_sys::{SpectreAlgorithm, SpectreUserKey}; use spectre_api_sys::{SpectreAlgorithm, SpectreKeyID, SpectreUserKey};
#[cfg(feature = "limit-fps")] #[cfg(feature = "limit-fps")]
use crate::FRAME_DURATION_MIN; use crate::FRAME_DURATION_MIN;
use crate::{ use crate::{
PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
db::{AcidDatabase, DbKey, DbPathSpectreUsers, PartitionAcid, ReadTransactionExt}, db::{AcidDatabase, DbKey, DbPathSpectreUsers, PartitionAcid, ReadTransactionExt},
ffi::alloc::__spre_free, ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
ui::{ ui::{
backend::SlintBackend, backend::SlintBackend,
messages::{ messages::{
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers, CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers,
}, },
storage::SpectreUsersConfig, storage::{SpectreUserConfig, SpectreUsersConfig},
}, },
util::DurationExt, util::DurationExt,
}; };
@ -155,13 +157,14 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
let main = AppWindow::new().unwrap(); let main = AppWindow::new().unwrap();
let state = State::new(db, main).await; 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 { struct State {
window: AppWindow, window: AppWindow,
db: AcidDatabase, db: Rc<AcidDatabase>,
users: SpectreUsersConfig, users: SpectreUsersConfig,
/// Currently active view. /// Currently active view.
view: AppState, view: AppState,
@ -177,18 +180,11 @@ impl State {
let state = Rc::new(RefCell::new(State { let state = Rc::new(RefCell::new(State {
window: main.clone_strong(), window: main.clone_strong(),
users: { users: {
let read = db.read_transaction().await; let users = Self::load_users(&db).await;
let mut buffer = vec![0_u8; 128]; warn!("Users: {users:#?}");
match read users
.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:?}"),
}
}, },
db, db: Rc::new(db),
view: AppState::Login, view: AppState::Login,
state_login: Default::default(), state_login: Default::default(),
state_users: Default::default(), state_users: Default::default(),
@ -206,56 +202,61 @@ impl State {
main.on_escape({ main.on_escape({
let state = state.clone(); let state = state.clone();
move || { move || {
state State::process_callback_message(&state, CallbackMessage::Escape);
.borrow_mut()
.process_callback_message(CallbackMessage::Escape);
} }
}); });
main.on_login_pw_accepted({ main.on_login_pw_accepted({
let state = state.clone(); let state = state.clone();
move |username, password| { move |username, password| {
state State::process_callback_message(
.borrow_mut() &state,
.process_callback_message(CallbackMessage::Login( CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }),
CallbackMessageLogin::PwAccepted { username, password }, );
));
} }
}); });
main.on_users_edit_user({ main.on_users_edit_user({
let state = state.clone(); let state = state.clone();
move |username, new| { move |username, new| {
state State::process_callback_message(
.borrow_mut() &state,
.process_callback_message(CallbackMessage::Users( CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }),
CallbackMessageUsers::EditUser { username, new }, );
));
} }
}); });
main.on_user_edit_compute_identicon({ main.on_user_edit_compute_identicon({
let state = state.clone(); let state = state.clone();
move |encrypted_key, password| { move |password| {
state State::process_callback_message(
.borrow_mut() &state,
.process_callback_message(CallbackMessage::UserEdit( CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon {
CallbackMessageUserEdit::ComputeIdenticon {
encrypted_key,
password, 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({ main.on_user_edit_confirm({
let state = state.clone(); let state = state.clone();
move |encrypted_key| { move |encrypted_key| {
state State::process_callback_message(
.borrow_mut() &state,
.process_callback_message(CallbackMessage::UserEdit( CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest {
CallbackMessageUserEdit::Confirm { encrypted_key }, encrypted_key,
)); }),
);
} }
}); });
@ -272,12 +273,36 @@ impl State {
state state
} }
fn process_callback_message(&mut self, message: CallbackMessage) { async fn load_users(db: &AcidDatabase) -> SpectreUsersConfig {
match self.view { let read = db.read_transaction().await;
AppState::Login => StateLogin::process_callback_message(self, message), let mut buffer = vec![0_u8; 128];
AppState::Users => StateUsers::process_callback_message(self, message), match read
AppState::UserEdit => StateUserEdit::process_callback_message(self, message), .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
AppState::UserSites => StateUserSites::process_callback_message(self, message), .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),
} }
} }
@ -298,8 +323,8 @@ impl State {
/// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve /// 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. /// async by having only one iteration of the loop run, and `await`ing here.
/// The following block is analogous to `main.run()`. /// The following block is analogous to `main.run()`.
async fn run_event_loop(&self) -> ! { async fn run_event_loop(window: AppWindow) -> ! {
self.window.show().unwrap(); window.show().unwrap();
loop { loop {
slint::run_event_loop().unwrap(); slint::run_event_loop().unwrap();
@ -310,19 +335,20 @@ impl State {
} }
#[expect(unreachable_code)] #[expect(unreachable_code)]
self.window.hide().unwrap(); window.hide().unwrap();
} }
} }
trait AppViewTrait { trait AppViewTrait {
fn process_callback_message(_state: &mut State, _message: CallbackMessage) {} fn process_callback_message(_state_rc: &Rc<RefCell<State>>, _message: CallbackMessage) {}
} }
#[derive(Default)] #[derive(Default)]
struct StateLogin {} struct StateLogin {}
impl AppViewTrait for StateLogin { impl AppViewTrait for StateLogin {
fn process_callback_message(state: &mut State, message: CallbackMessage) { fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
let mut state = state_rc.borrow_mut();
if let CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) = if let CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) =
message message
{ {
@ -372,7 +398,8 @@ impl AppViewTrait for StateLogin {
struct StateUsers {} struct StateUsers {}
impl AppViewTrait for StateUsers { impl AppViewTrait for StateUsers {
fn process_callback_message(state: &mut State, message: CallbackMessage) { fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
let mut state = state_rc.borrow_mut();
match message { match message {
CallbackMessage::Escape => { CallbackMessage::Escape => {
state.set_view(AppState::Login, false); state.set_view(AppState::Login, false);
@ -381,7 +408,8 @@ impl AppViewTrait for StateUsers {
state.state_user_edit = StateUserEdit { state.state_user_edit = StateUserEdit {
username: username.clone(), username: username.clone(),
new, new,
hashed: None, password: None,
encrypted_key: None,
}; };
state.window.set_user_edit_username(username); state.window.set_user_edit_username(username);
state.set_view(AppState::UserEdit, false); state.set_view(AppState::UserEdit, false);
@ -395,19 +423,19 @@ impl AppViewTrait for StateUsers {
struct StateUserEdit { struct StateUserEdit {
username: SharedString, username: SharedString,
new: bool, new: bool,
hashed: Option<ProposedPassword>, password: Option<SharedString>,
encrypted_key: Option<(Key, SpectreUserKey)>,
} }
impl AppViewTrait for StateUserEdit { impl AppViewTrait for StateUserEdit {
fn process_callback_message(state: &mut State, message: CallbackMessage) { fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
let state = state_rc.clone();
let mut state = state.borrow_mut();
match message { match message {
CallbackMessage::Escape => { CallbackMessage::Escape => {
state.set_view(AppState::Users, false); state.set_view(AppState::Users, false);
} }
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password }) => {
encrypted_key,
password,
}) => {
let username_c = CString::new(&*state.state_user_edit.username) let username_c = CString::new(&*state.state_user_edit.username)
.expect("Username cannot be converted to a C string."); .expect("Username cannot be converted to a C string.");
let password_c = let password_c =
@ -431,24 +459,107 @@ impl AppViewTrait for StateUserEdit {
warn!("Identicon: {identicon} ({identicon:?})"); warn!("Identicon: {identicon} ({identicon:?})");
state.window.set_user_edit_identicon(identicon.clone()); state.window.set_user_edit_identicon(identicon.clone());
state.state_user_edit.hashed = Some(ProposedPassword { state.state_user_edit.password = Some(password);
encrypted_key,
identicon,
})
} }
CallbackMessage::UserEdit(CallbackMessageUserEdit::Confirm { encrypted_key: _ }) => { CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId {
// TODO 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 { struct StateUserSites {
username: SharedString, username: SharedString,
user_key: SpectreUserKey, user_key: SpectreUserKey,

View file

@ -2,6 +2,7 @@ use alloc::{string::String, vec::Vec};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use password_hash::Key; use password_hash::Key;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use slint::SharedString;
use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType}; use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType};
#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)] #[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
@ -11,7 +12,7 @@ pub struct SpectreUsersConfig {
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub struct SpectreUserConfig { pub struct SpectreUserConfig {
pub username: String, pub username: SharedString,
#[serde(with = "serde_bytes")] #[serde(with = "serde_bytes")]
pub encrypted_key: Key, pub encrypted_key: Key,
#[serde(with = "serde_bytes")] #[serde(with = "serde_bytes")]
@ -26,10 +27,10 @@ pub struct SpectreSiteConfig {
#[serde(rename = "type")] #[serde(rename = "type")]
#[serde(with = "with_repr")] #[serde(with = "with_repr")]
pub result_type: SpectreResultType, pub result_type: SpectreResultType,
pub password: Option<String>, pub password: Option<SharedString>,
#[serde(with = "with_repr")] #[serde(with = "with_repr")]
pub login_type: SpectreResultType, pub login_type: SpectreResultType,
pub login_name: Option<String>, pub login_name: Option<SharedString>,
pub uses: u32, pub uses: u32,
pub last_used: NaiveDateTime, pub last_used: NaiveDateTime,
} }
@ -51,8 +52,8 @@ impl Default for SpectreSiteConfig {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpectreSite { pub struct SpectreSite {
pub username: String, pub username: SharedString,
pub site_name: String, pub site_name: SharedString,
pub config: SpectreSiteConfig, pub config: SpectreSiteConfig,
} }

View file

@ -43,8 +43,11 @@ export component AppWindow inherits Window {
callback users_edit_user <=> users_view.edit_user; callback users_edit_user <=> users_view.edit_user;
// User Edit View // User Edit View
in property <string> user_edit_username <=> user_edit_view.username; in property <string> user_edit_username <=> user_edit_view.username;
in property <string> user_edit_key_error <=> user_edit_view.key_error;
in-out property <string> user_edit_identicon <=> user_edit_view.identicon; in-out property <string> user_edit_identicon <=> user_edit_view.identicon;
in-out property <string> user_edit_key_id <=> user_edit_view.key_id;
callback user_edit_compute_identicon <=> user_edit_view.compute_identicon; 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; callback user_edit_confirm <=> user_edit_view.confirm;
// User Sites View // User Sites View
in property <[StandardListViewItem]> sites <=> user_sites_view.model; in property <[StandardListViewItem]> sites <=> user_sites_view.model;

View file

@ -6,8 +6,11 @@ export component UserEditView inherits HorizontalLayout {
padding: Style.spacing; padding: Style.spacing;
spacing: Style.spacing; spacing: Style.spacing;
in property <string> username; in property <string> username;
in property <string> key_error;
in-out property <string> identicon; in-out property <string> identicon;
callback compute_identicon(encrypted_key: string, password: string); in-out property <string> key_id;
callback compute_identicon(password: string);
callback compute_key_id(encrypted_key: string);
callback confirm(encrypted_key: string); callback confirm(encrypted_key: string);
callback cancel <=> button_cancel.clicked; callback cancel <=> button_cancel.clicked;
VerticalLayout { VerticalLayout {
@ -18,25 +21,16 @@ export component UserEditView inherits HorizontalLayout {
text: "Enter " + username + "'s encrypted key and press Enter:"; 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 { line_edit_password := LineEdit {
input-type: InputType.text; input-type: InputType.password;
placeholder-text: "Password"; placeholder-text: "Password";
edited(text) => { edited(text) => {
root.identicon = ""; root.identicon = "";
root.key_id = "";
} }
accepted(text) => { 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); 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 { HorizontalLayout {
spacing: Style.spacing; spacing: Style.spacing;
button_cancel := Button { button_cancel := Button {
@ -52,7 +63,7 @@ export component UserEditView inherits HorizontalLayout {
button_confirm := Button { button_confirm := Button {
text: "Confirm"; text: "Confirm";
enabled: !identicon.is-empty; enabled: !identicon.is-empty && !key_id.is-empty;
clicked => { clicked => {
confirm(line_edit_encrypted_key.text); confirm(line_edit_encrypted_key.text);
} }