Implement OTP clipboard and QR TUI

This commit is contained in:
Hermes Agent
2026-08-10 11:11:53 +00:00
parent 1850846696
commit 71bbff934e
8 changed files with 1021 additions and 28 deletions

View File

@@ -6,9 +6,11 @@ use crossterm::event::{KeyCode, KeyModifiers};
use ironstorage::{
command::{
CopyRequest, GenerateRequest, GeneratedPresentation, GrepRequest, InitRequest, InsertInput,
InsertRequest, MoveRequest, RemoveRequest,
InsertRequest, MoveRequest, OtpAppendRequest, OtpInputSource, OtpInsertRequest,
RemoveRequest,
},
crypto::KeyInfo,
otp::OtpInput,
write::{InsertContent, OverwriteDecision},
};
use zeroize::Zeroize;
@@ -135,6 +137,23 @@ pub struct TransferForm {
focus: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpFormKind {
Insert,
Append,
Validate,
}
#[derive(Debug)]
pub struct OtpForm {
kind: OtpFormKind,
entry: String,
uri: SecretText,
force: bool,
confirmed: bool,
focus: usize,
}
#[derive(Debug)]
pub enum WorkflowForm {
Init(InitForm),
@@ -144,6 +163,7 @@ pub enum WorkflowForm {
Remove(RemoveForm),
Move(TransferForm),
Copy(TransferForm),
Otp(OtpForm),
}
#[derive(Debug)]
@@ -168,6 +188,17 @@ pub enum WorkflowSubmission {
request: CopyRequest,
overwrite: OverwriteDecision,
},
OtpInsert {
request: OtpInsertRequest,
input: OtpInput,
},
OtpAppend {
request: OtpAppendRequest,
input: OtpInput,
},
OtpValidate {
uri: OtpInput,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -179,6 +210,16 @@ pub enum WorkflowInput {
}
impl WorkflowForm {
pub fn otp(kind: OtpFormKind, entry: Option<String>, force: bool) -> Self {
Self::Otp(OtpForm {
kind,
entry: entry.unwrap_or_default(),
uri: SecretText::default(),
force,
confirmed: false,
focus: 0,
})
}
pub fn init(keys: &[KeyInfo], default: Option<&KeyInfo>, request: Option<InitRequest>) -> Self {
let request = request.unwrap_or_else(|| InitRequest {
path: None,
@@ -328,6 +369,11 @@ impl WorkflowForm {
Self::Remove(_) => "Remove entry or folder",
Self::Move(_) => "Move or rename",
Self::Copy(_) => "Copy entry or folder",
Self::Otp(form) => match form.kind {
OtpFormKind::Insert => "Insert OTP URI",
OtpFormKind::Append => "Append OTP URI",
OtpFormKind::Validate => "Validate OTP URI",
},
}
}
@@ -424,6 +470,48 @@ impl WorkflowForm {
yes_no(form.overwrite),
),
],
Self::Otp(form) => {
vec![
row(
form.focus == 0,
"Entry",
if form.kind == OtpFormKind::Validate {
"(validation only)"
} else if form.entry.is_empty() {
"(derive from URI)"
} else {
&form.entry
},
),
row(
form.focus == 1,
"OTP URI",
if form.uri.is_empty() {
"(empty)"
} else {
"••••••••"
},
),
row(
form.focus == 2,
"Force replacement",
if form.kind == OtpFormKind::Validate {
"(not used)"
} else {
yes_no(form.force)
},
),
row(
form.focus == 3,
"Confirm write",
if form.kind == OtpFormKind::Validate {
"(not required)"
} else {
yes_no(form.confirmed)
},
),
]
}
}
}
@@ -573,6 +661,47 @@ impl WorkflowForm {
overwrite,
})
}
Self::Otp(form) => {
if form.uri.is_empty() {
return Err("OTP URI is required".to_owned());
}
let input = OtpInput::line(form.uri.bytes()).map_err(|error| error.to_string())?;
match form.kind {
OtpFormKind::Validate => Ok(WorkflowSubmission::OtpValidate { uri: input }),
OtpFormKind::Insert => {
if !form.confirmed {
return Err("Explicitly confirm the OTP write".to_owned());
}
Ok(WorkflowSubmission::OtpInsert {
request: OtpInsertRequest {
entry: (!form.entry.trim().is_empty())
.then(|| form.entry.trim().to_owned()),
force: form.force,
echo: false,
source: OtpInputSource::Uri,
},
input,
})
}
OtpFormKind::Append => {
if form.entry.trim().is_empty() {
return Err("Entry path is required".to_owned());
}
if !form.confirmed {
return Err("Explicitly confirm the OTP write".to_owned());
}
Ok(WorkflowSubmission::OtpAppend {
request: OtpAppendRequest {
entry: form.entry.trim().to_owned(),
force: form.force,
echo: false,
source: OtpInputSource::Uri,
},
input,
})
}
}
}
}
}
@@ -582,6 +711,7 @@ impl WorkflowForm {
Self::Insert(_) | Self::Generate(_) => 5,
Self::Grep(_) => 5,
Self::Remove(_) | Self::Move(_) | Self::Copy(_) => 4,
Self::Otp(_) => 4,
}
}
@@ -594,6 +724,7 @@ impl WorkflowForm {
Self::Grep(form) => &mut form.focus,
Self::Remove(form) => &mut form.focus,
Self::Move(form) | Self::Copy(form) => &mut form.focus,
Self::Otp(form) => &mut form.focus,
};
*focus = if forward {
(*focus + 1) % count
@@ -631,6 +762,8 @@ impl WorkflowForm {
form.destination_directory ^= true
}
Self::Move(form) | Self::Copy(form) if form.focus == 3 => form.overwrite ^= true,
Self::Otp(form) if form.focus == 2 => form.force ^= true,
Self::Otp(form) if form.focus == 3 => form.confirmed ^= true,
_ => self.character(' '),
}
}
@@ -674,6 +807,12 @@ impl WorkflowForm {
Self::Move(form) | Self::Copy(form) if form.focus == 1 => {
form.destination.pop();
}
Self::Otp(form) if form.focus == 0 => {
form.entry.pop();
}
Self::Otp(form) if form.focus == 1 => {
form.uri.pop();
}
_ => {}
}
}
@@ -702,6 +841,8 @@ impl WorkflowForm {
Self::Move(form) | Self::Copy(form) if form.focus == 1 => {
form.destination.push(character)
}
Self::Otp(form) if form.focus == 0 => form.entry.push(character),
Self::Otp(form) if form.focus == 1 && character != '\n' => form.uri.push(character),
_ => {}
}
}