Site password derivation
This commit is contained in:
parent
4a5ada0bb0
commit
1e2d43a628
|
|
@ -1,4 +1,8 @@
|
|||
use slint::SharedString;
|
||||
use alloc::rc::Rc;
|
||||
use slint::{SharedString, VecModel};
|
||||
use spectre_api_sys::SpectreUserKey;
|
||||
|
||||
use crate::ui::storage::SpectreSite;
|
||||
|
||||
pub enum CallbackMessage {
|
||||
/// The escape key was pressed.
|
||||
|
|
@ -9,12 +13,24 @@ pub enum CallbackMessage {
|
|||
UserSites(CallbackMessageUserSites),
|
||||
}
|
||||
|
||||
pub enum LoginResult {
|
||||
Failure,
|
||||
Success {
|
||||
user_key: SpectreUserKey,
|
||||
sites: Rc<VecModel<SpectreSite>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum CallbackMessageLogin {
|
||||
PwAccepted {
|
||||
user_index: i32,
|
||||
username: SharedString,
|
||||
password: SharedString,
|
||||
},
|
||||
LoginResult {
|
||||
username: SharedString,
|
||||
result: LoginResult,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum CallbackMessageUsers {
|
||||
|
|
@ -24,8 +40,11 @@ pub enum CallbackMessageUsers {
|
|||
pub enum CallbackMessageUserEdit {
|
||||
ComputeIdenticon { password: SharedString },
|
||||
ComputeKeyId { key: SharedString },
|
||||
ConfirmRequest { encrypted_key: SharedString },
|
||||
ConfirmRequest,
|
||||
ConfirmProcessed,
|
||||
}
|
||||
|
||||
pub enum CallbackMessageUserSites {}
|
||||
pub enum CallbackMessageUserSites {
|
||||
SiteNameEdited { query: SharedString },
|
||||
SiteNameAccepted { site_list_index: i32 },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,46 @@
|
|||
// #![cfg_attr(not(feature = "simulator"), no_main)]
|
||||
|
||||
use core::{cell::RefCell, ffi::CStr, ops::DerefMut};
|
||||
use core::{cell::RefCell, ffi::CStr, ops::DerefMut, pin::Pin};
|
||||
|
||||
use alloc::{boxed::Box, ffi::CString, format, rc::Rc, vec};
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
boxed::Box,
|
||||
ffi::CString,
|
||||
format,
|
||||
rc::Rc,
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
};
|
||||
use embassy_time::Instant;
|
||||
use hex::FromHexError;
|
||||
use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer};
|
||||
use log::{error, info, warn};
|
||||
use password_hash::Key;
|
||||
use slint::{Model, ModelExt, ModelRc, SharedString, VecModel};
|
||||
use spectre_api_sys::{SpectreAlgorithm, SpectreKeyID, SpectreUserKey};
|
||||
use slint::{
|
||||
Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem,
|
||||
VecModel,
|
||||
};
|
||||
use spectre_api_sys::{
|
||||
SpectreAlgorithm, SpectreCounter, SpectreKeyID, SpectreKeyPurpose, SpectreResultType,
|
||||
SpectreSiteKey, 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},
|
||||
db::{
|
||||
AcidDatabase, DbKey, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid,
|
||||
ReadTransactionExt,
|
||||
},
|
||||
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
|
||||
ui::{
|
||||
backend::SlintBackend,
|
||||
messages::{
|
||||
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers,
|
||||
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit,
|
||||
CallbackMessageUserSites, CallbackMessageUsers, LoginResult,
|
||||
},
|
||||
storage::{SpectreUserConfig, SpectreUsersConfig},
|
||||
storage::{SpectreSite, SpectreSiteConfig, SpectreUserConfig, SpectreUsersConfig},
|
||||
},
|
||||
util::DurationExt,
|
||||
};
|
||||
|
|
@ -66,95 +85,30 @@ fn spectre_derive_user_key(
|
|||
}
|
||||
}
|
||||
|
||||
fn spectre_derive_site_password(user_key: &SpectreUserKey, site_name: &CStr) -> String {
|
||||
unsafe {
|
||||
let site_password_c = &*spectre_api_sys::spectre_site_result(
|
||||
user_key as *const SpectreUserKey,
|
||||
site_name.as_ptr(),
|
||||
SpectreResultType::SpectreResultDefaultResult,
|
||||
core::ptr::null(),
|
||||
SpectreCounter::Initial,
|
||||
SpectreKeyPurpose::Authentication,
|
||||
core::ptr::null(),
|
||||
);
|
||||
let site_password = CStr::from_ptr(site_password_c)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
__spre_free(site_password_c as *const _ as *mut _);
|
||||
site_password
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
|
||||
let db = AcidDatabase::mount(flash_part_acid).await;
|
||||
|
||||
// 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");
|
||||
|
||||
// TODO:
|
||||
// * Store a config as a versioned postcard-serialized struct
|
||||
// * Store accounts and sites as ranges in the DB
|
||||
|
|
@ -269,11 +223,31 @@ impl State {
|
|||
|
||||
main.on_user_edit_confirm({
|
||||
let state = state.clone();
|
||||
move |encrypted_key| {
|
||||
move |_encrypted_key| {
|
||||
State::process_callback_message(
|
||||
&state,
|
||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest {
|
||||
encrypted_key,
|
||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
main.on_user_sites_site_name_edited({
|
||||
let state = state.clone();
|
||||
move |query| {
|
||||
State::process_callback_message(
|
||||
&state,
|
||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
main.on_user_sites_site_name_accepted({
|
||||
let state = state.clone();
|
||||
move |site_list_index| {
|
||||
State::process_callback_message(
|
||||
&state,
|
||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
||||
site_list_index,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -315,6 +289,44 @@ impl State {
|
|||
write.commit().await.unwrap();
|
||||
}
|
||||
|
||||
fn make_site_list(
|
||||
sites: &Rc<VecModel<SpectreSite>>,
|
||||
query: &SharedString,
|
||||
) -> Rc<VecModel<SiteListEntry>> {
|
||||
let site_list = Rc::new(VecModel::default());
|
||||
|
||||
for site in sites.iter() {
|
||||
site_list.push(SiteListEntry::Existing(site.site_name));
|
||||
}
|
||||
|
||||
if !query.is_empty() {
|
||||
site_list.push(SiteListEntry::New(query.clone()))
|
||||
}
|
||||
|
||||
site_list
|
||||
}
|
||||
|
||||
fn update_site_list(&mut self) {
|
||||
if let Some(StateUserSites {
|
||||
sites,
|
||||
query,
|
||||
site_list,
|
||||
..
|
||||
}) = self.state_user_sites.as_mut()
|
||||
{
|
||||
*site_list = State::make_site_list(sites, query);
|
||||
self.window
|
||||
.set_user_sites_sites(ModelRc::from(Rc::new(site_list.clone().map(|site| {
|
||||
let mut item = StandardListViewItem::default();
|
||||
item.text = site.to_string();
|
||||
item
|
||||
}))));
|
||||
} else {
|
||||
self.window.invoke_user_sites_site_name_clear();
|
||||
self.window.set_user_sites_sites(Default::default());
|
||||
}
|
||||
}
|
||||
|
||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
||||
let view = state_rc.borrow().view;
|
||||
match view {
|
||||
|
|
@ -376,49 +388,149 @@ 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 {
|
||||
match message {
|
||||
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(&*user.username).expect("Username cannot be converted to a C string.");
|
||||
let username_c = CString::new(&*user.username)
|
||||
.expect("Username cannot be converted to a C string.");
|
||||
let password_c =
|
||||
CString::new(&*password).expect("Password cannot be converted to a C string.");
|
||||
let user_key =
|
||||
spectre_derive_user_key(&username_c, &password_c, Some(user.encrypted_key));
|
||||
|
||||
// let 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 mut write = db.write_transaction().await;
|
||||
// write
|
||||
// .write(
|
||||
// &DbKey::new(DbPathSpectreUserSite {
|
||||
// user_sites: DbPathSpectreUserSites {
|
||||
// username: "test".into(),
|
||||
// },
|
||||
// site: "example.org".into(),
|
||||
// }),
|
||||
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// write
|
||||
// .write(
|
||||
// &DbKey::new(DbPathSpectreUserSite {
|
||||
// user_sites: DbPathSpectreUserSites {
|
||||
// username: "test".into(),
|
||||
// },
|
||||
// site: "sub.example.org".into(),
|
||||
// }),
|
||||
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// write
|
||||
// .write(
|
||||
// &DbKey::new(DbPathSpectreUsers),
|
||||
// &postcard::to_allocvec(&value).unwrap(),
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// write.commit().await.unwrap();
|
||||
// }
|
||||
|
||||
if user.key_id == user_key.keyID.bytes {
|
||||
info!("Correct password entered for user {:?}.", user.username);
|
||||
state.state_user_sites = Some(StateUserSites {
|
||||
if user.key_id != user_key.keyID.bytes {
|
||||
State::process_callback_message(
|
||||
&state_rc,
|
||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
||||
username: user.username,
|
||||
user_key,
|
||||
});
|
||||
state.set_view(AppState::UserSites, true, false);
|
||||
} else {
|
||||
warn!("Incorrect password entered for user {:?}.", user.username);
|
||||
result: LoginResult::Failure,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
slint::spawn_local({
|
||||
let state_rc = state_rc.clone();
|
||||
let username = user.username.clone();
|
||||
let db = state.db.clone();
|
||||
|
||||
async move {
|
||||
let read = db.read_transaction().await;
|
||||
// TODO: https://github.com/embassy-rs/ekv/issues/20
|
||||
let mut buffer = vec![0; 256];
|
||||
// let slice = read
|
||||
// .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let read_value = postcard::from_bytes::<SpectreUsersConfig>(&slice).unwrap();
|
||||
|
||||
let key_sites = DbKey::new(DbPathSpectreUserSites {
|
||||
username: Cow::Borrowed(&username),
|
||||
});
|
||||
let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE];
|
||||
let mut cursor = read
|
||||
.read_range(key_sites.range_of_children())
|
||||
.await
|
||||
.unwrap();
|
||||
let sites = VecModel::default();
|
||||
|
||||
while let Some((key_len, value_len)) =
|
||||
cursor.next(&mut key_buffer, &mut buffer).await.unwrap()
|
||||
{
|
||||
let key = DbKey::from_raw(key_buffer[..key_len].into());
|
||||
let mut key_segments = key.segments();
|
||||
let value = &buffer[..value_len];
|
||||
let site_config =
|
||||
postcard::from_bytes::<SpectreSiteConfig>(&value).unwrap();
|
||||
let site = SpectreSite {
|
||||
config: site_config,
|
||||
username: key_segments.nth(2).unwrap().as_ref().into(),
|
||||
site_name: key_segments.nth(1).unwrap().as_ref().into(),
|
||||
};
|
||||
|
||||
info!("site = {:#?}", site);
|
||||
sites.push(site);
|
||||
}
|
||||
|
||||
State::process_callback_message(
|
||||
&state_rc,
|
||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
||||
username: user.username,
|
||||
result: LoginResult::Success {
|
||||
user_key,
|
||||
sites: Rc::new(sites),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
||||
username,
|
||||
result: LoginResult::Success { user_key, sites },
|
||||
}) => {
|
||||
info!("Correct password entered for user {:?}.", username);
|
||||
state.state_user_sites = Some(StateUserSites {
|
||||
username,
|
||||
user_key,
|
||||
query: SharedString::new(),
|
||||
sites: sites.clone(),
|
||||
site_list: Default::default(),
|
||||
});
|
||||
state.update_site_list();
|
||||
state.set_view(AppState::UserSites, true, false);
|
||||
}
|
||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
||||
username,
|
||||
result: LoginResult::Failure,
|
||||
}) => {
|
||||
warn!("Incorrect password entered for user {:?}.", username);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -538,9 +650,7 @@ impl AppViewTrait for StateUserEdit {
|
|||
);
|
||||
state.state_user_edit.encrypted_key = Some((key, user_key));
|
||||
}
|
||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest {
|
||||
encrypted_key: _,
|
||||
}) => {
|
||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest) => {
|
||||
let Some((
|
||||
encrypted_key,
|
||||
SpectreUserKey {
|
||||
|
|
@ -580,6 +690,7 @@ impl AppViewTrait for StateUserEdit {
|
|||
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(
|
||||
|
|
@ -602,6 +713,153 @@ impl AppViewTrait for StateUserEdit {
|
|||
struct StateUserSites {
|
||||
username: SharedString,
|
||||
user_key: SpectreUserKey,
|
||||
query: SharedString,
|
||||
sites: Rc<VecModel<SpectreSite>>,
|
||||
site_list: Rc<VecModel<SiteListEntry>>,
|
||||
}
|
||||
|
||||
impl AppViewTrait for StateUserSites {}
|
||||
impl AppViewTrait for StateUserSites {
|
||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
||||
let state = state_rc.clone();
|
||||
let mut state = state.borrow_mut();
|
||||
match message {
|
||||
CallbackMessage::Escape => {
|
||||
state.set_view(AppState::Login, true, false);
|
||||
}
|
||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }) => {
|
||||
if let Some(user_sites) = state.state_user_sites.as_mut() {
|
||||
user_sites.query = query;
|
||||
state.update_site_list();
|
||||
}
|
||||
}
|
||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
||||
site_list_index,
|
||||
}) => {
|
||||
let Some(user_sites) = state.state_user_sites.as_mut() else {
|
||||
error!("User sites uninitialized.");
|
||||
return;
|
||||
};
|
||||
let Some(site_list_entry) = user_sites.site_list.row_data(site_list_index as usize)
|
||||
else {
|
||||
error!("Invalid site list entry index: {site_list_index}");
|
||||
return;
|
||||
};
|
||||
warn!("Site name accepted: {site_list_entry:?}");
|
||||
let site_name = match site_list_entry {
|
||||
SiteListEntry::New(site_name) => site_name,
|
||||
SiteListEntry::Existing(site_name) => site_name,
|
||||
};
|
||||
let site_name_c = CString::new(&*site_name).unwrap();
|
||||
let site_password =
|
||||
spectre_derive_site_password(&user_sites.user_key, &site_name_c);
|
||||
|
||||
warn!("Site password: {site_password:?}");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SitesModel<M>(Pin<Box<ModelChangeListenerContainer<SitesModelInner<M>>>>)
|
||||
where
|
||||
M: Model + 'static;
|
||||
|
||||
struct SitesModelInner<M>
|
||||
where
|
||||
M: Model + 'static,
|
||||
{
|
||||
wrapped_model: M,
|
||||
notify: ModelNotify,
|
||||
}
|
||||
|
||||
impl<M> ModelChangeListener for SitesModelInner<M>
|
||||
where
|
||||
M: Model + 'static,
|
||||
{
|
||||
fn row_changed(self: Pin<&Self>, row: usize) {
|
||||
self.notify
|
||||
.row_changed(self.wrapped_model.row_count() - 1 - row);
|
||||
}
|
||||
|
||||
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
||||
let row_count = self.wrapped_model.row_count();
|
||||
let old_row_count = row_count - count;
|
||||
let index = old_row_count - index;
|
||||
self.notify.row_added(index, count);
|
||||
}
|
||||
|
||||
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
||||
let row_count = self.wrapped_model.row_count();
|
||||
self.notify.row_removed(row_count - index, count);
|
||||
}
|
||||
|
||||
fn reset(self: Pin<&Self>) {
|
||||
self.notify.reset()
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> SitesModel<M>
|
||||
where
|
||||
M: Model + 'static,
|
||||
{
|
||||
pub fn new(wrapped_model: M) -> Self {
|
||||
let inner = SitesModelInner {
|
||||
wrapped_model,
|
||||
notify: Default::default(),
|
||||
};
|
||||
let container = Box::pin(ModelChangeListenerContainer::new(inner));
|
||||
container
|
||||
.wrapped_model
|
||||
.model_tracker()
|
||||
.attach_peer(container.as_ref().model_peer());
|
||||
Self(container)
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner model
|
||||
pub fn source_model(&self) -> &M {
|
||||
&self.0.as_ref().get().get_ref().wrapped_model
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Model for SitesModel<M>
|
||||
where
|
||||
M: Model + 'static,
|
||||
{
|
||||
type Data = M::Data;
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
self.0.wrapped_model.row_count()
|
||||
}
|
||||
|
||||
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
||||
let count = self.0.wrapped_model.row_count();
|
||||
self.0.wrapped_model.row_data(count.checked_sub(row + 1)?)
|
||||
}
|
||||
fn set_row_data(&self, row: usize, data: Self::Data) {
|
||||
let count = self.0.as_ref().wrapped_model.row_count();
|
||||
self.0.wrapped_model.set_row_data(count - row - 1, data);
|
||||
}
|
||||
|
||||
fn model_tracker(&self) -> &dyn ModelTracker {
|
||||
&self.0.notify
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn core::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SiteListEntry {
|
||||
New(SharedString),
|
||||
Existing(SharedString),
|
||||
}
|
||||
|
||||
impl SiteListEntry {
|
||||
pub fn to_string(&self) -> SharedString {
|
||||
match self {
|
||||
SiteListEntry::New(site) => slint::format!("{site} <Add new site>"),
|
||||
SiteListEntry::Existing(site) => site.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,12 @@ export component AppWindow inherits Window {
|
|||
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;
|
||||
callback site_pw_edited <=> user_sites_view.pw_edited;
|
||||
callback site_pw_accepted <=> user_sites_view.pw_accepted;
|
||||
in property <[StandardListViewItem]> user_sites_sites <=> user_sites_view.model;
|
||||
callback user_sites_site_name_edited <=> user_sites_view.site_name_edited;
|
||||
callback user_sites_site_name_accepted <=> user_sites_view.site_name_accepted;
|
||||
public function user_sites_site_name_clear() {
|
||||
user_sites_view.site_name_clear();
|
||||
}
|
||||
focus-scope := FocusScope {
|
||||
key-pressed(event) => {
|
||||
if event.text == "\u{1b}" {
|
||||
|
|
|
|||
|
|
@ -7,24 +7,32 @@ export component UserSitesView inherits HorizontalLayout {
|
|||
spacing: Style.spacing;
|
||||
in property <[StandardListViewItem]> model <=> list_view_sites.model;
|
||||
in-out property <int> current-item <=> list_view_sites.current-item;
|
||||
callback pw_edited <=> line_edit_site_pw.edited;
|
||||
callback pw_accepted <=> line_edit_site_pw.accepted;
|
||||
callback site_name_edited <=> line_edit_site_name.edited;
|
||||
callback site_name_accepted(site_list_index: int);
|
||||
public function site_name_clear() {
|
||||
line_edit_site_name.text = "";
|
||||
}
|
||||
FocusScope {
|
||||
key-pressed(event) => {
|
||||
if event.text == "\n" {
|
||||
site_name_accepted(list_view_sites.current-item);
|
||||
EventResult.accept
|
||||
} else {
|
||||
EventResult.reject
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
spacing: Style.spacing;
|
||||
Text {
|
||||
text: "Send password for:";
|
||||
}
|
||||
|
||||
line_edit_site_pw := LineEdit {
|
||||
line_edit_site_name := LineEdit {
|
||||
input-type: InputType.text;
|
||||
placeholder-text: "example.org";
|
||||
}
|
||||
|
||||
list_view_sites := StandardListView {
|
||||
model: [
|
||||
{ text: "Test" },
|
||||
{ text: "Test" },
|
||||
];
|
||||
list_view_sites := StandardListView { }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
firmware/password-hash/.cargo/config.toml
Normal file
2
firmware/password-hash/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[env] # These must be kept in sync with /.zed/settings.json
|
||||
LIBSODIUM_INSTALL_DIR = "../libsodium/install-host"
|
||||
|
|
@ -1,3 +1,17 @@
|
|||
# Compiling
|
||||
|
||||
Compile on Linux or in WSL.
|
||||
|
||||
Compile libsodium for the host:
|
||||
```bash
|
||||
cd ../libsodium
|
||||
./configure --disable-shared --enable-static --prefix="$PWD/install-host"
|
||||
make -j
|
||||
make install
|
||||
```
|
||||
|
||||
Then compile and run password-hash:
|
||||
```bash
|
||||
cd ../password-hash
|
||||
cargo run --release [--target x86_64-unknown-linux-gnu]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fn main() {
|
|||
libsodium_install_dir.join("lib/libsodium.a").display()
|
||||
);
|
||||
} else {
|
||||
println!("cargo:warn=Environment variable `LIBSODIUM_INSTALL_DIR` missing!");
|
||||
panic!("Environment variable `LIBSODIUM_INSTALL_DIR` missing!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue