Implement TUI authentication and inactivity relock
This commit is contained in:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user