Implement TUI authentication and inactivity relock

This commit is contained in:
Hermes Agent
2026-08-10 06:22:23 +00:00
parent 91f58bae3a
commit 57a79b7d21
6 changed files with 365 additions and 19 deletions

View File

@@ -18,7 +18,7 @@ use ratatui::DefaultTerminal;
use crate::{
action::resolve_key,
app::{App, AppEffect, AsyncPayload, StartupData},
runtime::AsyncExecutor,
runtime::{AsyncExecutor, AuthenticationCoordinator, AuthenticationEvent},
};
const TICK_INTERVAL: Duration = Duration::from_millis(250);
@@ -28,39 +28,97 @@ const TICK_INTERVAL: Duration = Duration::from_millis(250);
pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
let mut app = App::new();
let executor = AsyncExecutor::new();
let mut authentication = None;
let mut authentication_initialized = false;
let startup = app.begin_latest_request();
executor.submit(startup, || load_startup().map(AsyncPayload::Startup));
executor.submit(startup, || {
load_startup().map(|startup| AsyncPayload::Startup(Box::new(startup)))
});
while !app.should_quit() {
for result in executor.drain() {
app.apply_result(result);
}
if !authentication_initialized
&& let (Some(config), Some(key)) = (app.config(), app.default_key())
{
authentication_initialized = true;
match AuthenticationCoordinator::system(config.authentication_timeout(), key.clone()) {
Ok(coordinator) => authentication = Some(coordinator),
Err(error) => app.authentication_failed(error),
}
}
if let Some(coordinator) = authentication.as_mut()
&& let Some(event) = coordinator.completion()
{
apply_authentication_event(&mut app, event);
}
let size = terminal.size()?;
app.resize(size.width, size.height);
terminal.draw(|frame| ui::draw(frame, &app))?;
if !event::poll(TICK_INTERVAL)? {
app.tick();
if let Some(coordinator) = authentication.as_mut() {
if let Some(event) = coordinator.poll_lease() {
apply_authentication_event(&mut app, event);
}
app.update_remaining_lease(coordinator.remaining_time());
}
continue;
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if let Some(coordinator) = authentication.as_mut()
&& let Some(event) = coordinator.touch_user_activity()
{
apply_authentication_event(&mut app, event);
}
if app.sidebar().is_editing_filter() {
if handle_filter_key(&mut app, key.code) {
submit_filter(&mut app, &executor);
}
} else if let Some(action) = resolve_key(app.mode(), key.code, key.modifiers)
&& app.dispatch(action) == AppEffect::RefreshTree
&& let Some(config) = app.config().cloned()
{
let token = app.begin_latest_request();
executor.submit(token, move || {
load_tree(&config).map(AsyncPayload::Refreshed)
});
} else if let Some(action) = resolve_key(app.mode(), key.code, key.modifiers) {
match app.dispatch(action) {
AppEffect::RefreshTree => {
if let Some(config) = app.config().cloned() {
let token = app.begin_latest_request();
executor.submit(token, move || {
load_tree(&config).map(AsyncPayload::Refreshed)
});
}
}
AppEffect::AuthenticateEntry(entry) => {
if let Some(coordinator) = authentication.as_mut() {
coordinator.request(entry);
} else {
app.authentication_failed(
"operating-system secure storage is unavailable".to_owned(),
);
}
}
AppEffect::ManualLock => {
if let Some(coordinator) = authentication.as_mut()
&& let Err(error) = coordinator.lock()
{
app.forced_relock("manual lock requested");
app.report_status(format!(
"locked after secure-store cleanup failed: {error}"
));
}
}
AppEffect::None => {}
}
}
}
Event::Resize(width, height) => app.resize(width, height),
Event::FocusLost => {
if let Some(coordinator) = authentication.as_mut() {
let _ignored = coordinator.lock();
}
app.forced_relock("terminal ownership was lost");
}
_ => {}
}
}
@@ -69,8 +127,31 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
fn load_startup() -> Result<StartupData, String> {
let config = ironstorage::config::Config::load(None).map_err(|error| error.to_string())?;
let tree = load_tree(&config)?;
Ok(StartupData { config, tree })
let repository = ironstorage::repository::Repository::open(config.vault())
.map_err(|error| error.to_string())?;
let keys = ironstorage::crypto::KeyStore::load(config.key_material())
.map_err(|error| error.to_string())?;
let key_handle = keys
.resolve(config.default_key().as_str())
.map_err(|error| error.to_string())?;
let key = keys
.infos()
.find(|key| key.fingerprint() == key_handle.fingerprint())
.ok_or_else(|| "the configured OpenPGP key is unavailable".to_owned())?;
let tree = ironstorage::read::VaultReader::new(&repository, &keys)
.list(&ironstorage::repository::DirectoryPath::root())
.map_err(|error| error.to_string())?;
Ok(StartupData { config, tree, key })
}
fn apply_authentication_event(app: &mut App, event: AuthenticationEvent) {
match event {
AuthenticationEvent::Granted(entry) => {
app.authentication_granted(entry);
}
AuthenticationEvent::Failed(error) => app.authentication_failed(error),
AuthenticationEvent::Expired => app.forced_relock("authentication lease expired"),
}
}
fn load_tree(config: &ironstorage::config::Config) -> Result<ironstorage::read::TreeModel, String> {