Fix password fields; show stored users on login page

This commit is contained in:
Jakub Hlusička 2026-02-08 01:17:33 +01:00
parent d4c8d69cf3
commit 4a5ada0bb0
5 changed files with 151 additions and 60 deletions

View file

@ -11,6 +11,7 @@ pub enum CallbackMessage {
pub enum CallbackMessageLogin {
PwAccepted {
user_index: i32,
username: SharedString,
password: SharedString,
},

View file

@ -5,9 +5,9 @@ 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 log::{error, info, warn};
use password_hash::Key;
use slint::SharedString;
use slint::{Model, ModelExt, ModelRc, SharedString, VecModel};
use spectre_api_sys::{SpectreAlgorithm, SpectreKeyID, SpectreUserKey};
#[cfg(feature = "limit-fps")]
@ -33,7 +33,17 @@ pub mod window_adapter;
slint::include_modules!();
fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey {
fn spectre_derive_user_key(
username: &CStr,
password: &CStr,
encrypted_key: Option<Key>,
) -> 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 {
@ -177,13 +187,16 @@ struct State {
impl State {
async fn new(db: AcidDatabase, main: AppWindow) -> Rc<RefCell<Self>> {
let state = Rc::new(RefCell::new(State {
window: main.clone_strong(),
users: {
let users = {
let users = Self::load_users(&db).await;
warn!("Users: {users:#?}");
users
},
};
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(),
@ -195,7 +208,7 @@ impl State {
main.on_enter_view({
let state = state.clone();
move |view| {
state.borrow_mut().set_view(view, true);
state.borrow_mut().set_view(view, true, true);
}
});
@ -206,12 +219,18 @@ impl State {
}
});
main.set_login_usernames(ModelRc::new(usernames));
main.on_login_pw_accepted({
let state = state.clone();
move |username, password| {
move |user_index, username, password| {
State::process_callback_message(
&state,
CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }),
CallbackMessage::Login(CallbackMessageLogin::PwAccepted {
user_index,
username,
password,
}),
);
}
});
@ -306,11 +325,7 @@ impl State {
}
}
fn set_view(&mut self, view: AppState, reset: bool) {
self.view = view;
self.window.set_app_state(view);
if reset {
fn reset_view(&mut self) {
match self.view {
AppState::Login => self.state_login = Default::default(),
AppState::Users => self.state_users = Default::default(),
@ -318,6 +333,18 @@ impl State {
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
@ -349,14 +376,22 @@ struct StateLogin {}
impl AppViewTrait for StateLogin {
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 }) =
message
if let CallbackMessage::Login(CallbackMessageLogin::PwAccepted {
user_index,
username: _,
password,
}) = message
{
let Some(user) = state.users.users.row_data(user_index as usize) else {
error!("Failed to find a user with index {user_index}.");
return;
};
let username_c =
CString::new(&*username).expect("Username cannot be converted to a C string.");
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);
let user_key =
spectre_derive_user_key(&username_c, &password_c, Some(user.encrypted_key));
// let site_key_start = Instant::now();
// let site_key = &*spectre_api_sys::spectre_site_key(
@ -373,22 +408,16 @@ impl AppViewTrait for StateLogin {
// );
// 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);
info!("Correct password entered for user {:?}.", user.username);
state.state_user_sites = Some(StateUserSites {
username: user.username,
user_key,
});
state.set_view(AppState::UserSites, true, false);
} else {
warn!("Incorrect password entered for user {username:?}.");
// TODO: Clear the input
warn!("Incorrect password entered for user {:?}.", user.username);
}
}
}
@ -402,7 +431,7 @@ impl AppViewTrait for StateUsers {
let mut state = state_rc.borrow_mut();
match message {
CallbackMessage::Escape => {
state.set_view(AppState::Login, false);
state.set_view(AppState::Login, true, false);
}
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => {
state.state_user_edit = StateUserEdit {
@ -412,7 +441,7 @@ impl AppViewTrait for StateUsers {
encrypted_key: None,
};
state.window.set_user_edit_username(username);
state.set_view(AppState::UserEdit, false);
state.set_view(AppState::UserEdit, true, false);
}
_ => (),
}
@ -433,7 +462,7 @@ impl AppViewTrait for StateUserEdit {
let mut state = state.borrow_mut();
match message {
CallbackMessage::Escape => {
state.set_view(AppState::Users, false);
state.set_view(AppState::Users, true, false);
}
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password }) => {
let username_c = CString::new(&*state.state_user_edit.username)
@ -493,14 +522,11 @@ impl AppViewTrait for StateUserEdit {
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);
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(
@ -526,17 +552,30 @@ impl AppViewTrait for StateUserEdit {
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,
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) {
if 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();
@ -553,7 +592,7 @@ impl AppViewTrait for StateUserEdit {
}
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed) => {
state.state_user_edit = Default::default();
state.set_view(AppState::Users, true);
state.set_view(AppState::Users, true, true);
}
_ => (),
}

View file

@ -1,13 +1,34 @@
use alloc::{string::String, vec::Vec};
use core::fmt::{Debug, Formatter};
use alloc::{rc::Rc, string::String, vec::Vec};
use chrono::NaiveDateTime;
use password_hash::Key;
use serde::{Deserialize, Serialize};
use slint::SharedString;
use slint::{Model, ModelRc, SharedString, VecModel};
use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType};
#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
#[derive(Deserialize, Serialize, Default)]
pub struct SpectreUsersConfig {
pub users: Vec<SpectreUserConfig>,
#[serde(with = "vec_model")]
pub users: Rc<VecModel<SpectreUserConfig>>,
}
impl Debug for SpectreUsersConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SpectreUsersConfig")
.field_with("users", |f| {
f.debug_list().entries(self.users.iter()).finish()
})
.finish()
}
}
impl Clone for SpectreUsersConfig {
fn clone(&self) -> Self {
Self {
users: Rc::new(self.users.iter().collect()),
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
@ -57,6 +78,33 @@ pub struct SpectreSite {
pub config: SpectreSiteConfig,
}
mod vec_model {
use alloc::{rc::Rc, vec::Vec};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use slint::{Model, VecModel};
#[allow(unused)]
pub fn serialize<T, S>(value: &Rc<VecModel<T>>, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
VecModel<T>: Model<Data = T>,
S: Serializer,
{
let vec: Vec<T> = value.iter().collect();
vec.serialize(serializer)
}
#[allow(unused)]
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Rc<VecModel<T>>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
let vec = Vec::<T>::deserialize(deserializer)?;
Ok(Rc::new(VecModel::from(vec)))
}
}
mod with_repr {
use serde::{Deserialize, Deserializer, Serializer};
use spectre_api_sys::{SpectreAlgorithm, SpectreResultType};

View file

@ -7,7 +7,7 @@ export component LoginView inherits VerticalLayout {
spacing: Style.spacing;
in property <[string]> usernames <=> combo_box_username.model;
callback users_clicked();
callback pw_accepted(string, string);
callback pw_accepted(int, string, string);
Rectangle { }
HorizontalLayout {
@ -25,14 +25,16 @@ export component LoginView inherits VerticalLayout {
input-type: InputType.password;
placeholder-text: "Password";
accepted(text) => {
root.pw_accepted(combo_box_username.current-value, text);
root.pw_accepted(combo_box_username.current-index, combo_box_username.current-value, text);
line_edit_user_pw.text = "";
}
}
IconButton {
icon: @image-url("images/log-in.svg");
clicked => {
root.pw_accepted(combo_box_username.current-value, line_edit_user_pw.text);
root.pw_accepted(combo_box_username.current-index, combo_box_username.current-value, line_edit_user_pw.text);
line_edit_user_pw.text = "";
}
}
}

View file

@ -27,7 +27,7 @@ export enum AppState {
export component AppWindow inherits Window {
// Special characters to generate pre-render glyphs for.
in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨";
in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨";
in property <string> dummy_identicon_symbols: "╔╚╰═█░▒▓☺☻╗╝╯═◈◎◐◑◒◓☀☁☂☃☄★☆☎☏⎈⌂☘☢☣☕⌚⌛⏰⚡⛄⛅☔♔♕♖♗♘♙♚♛♜♝♞♟♨♩♪♫⚐⚑⚔⚖⚙⚠⌘⏎✄✆✈✉✌";
default-font-family: "IBM Plex Mono";
default-font-size: 16pt;
@ -38,6 +38,7 @@ export component AppWindow inherits Window {
callback escape();
callback enter-view(view: AppState);
// Login View
in property <[string]> login_usernames <=> login_view.usernames;
callback login_pw_accepted <=> login_view.pw_accepted;
// Users View
callback users_edit_user <=> users_view.edit_user;