Present desktop OTP and sensitive outputs
This commit is contained in:
@@ -32,6 +32,13 @@ pub enum UiAction {
|
||||
CopyEntry,
|
||||
DeleteEntry,
|
||||
ToggleReveal,
|
||||
GenerateOtp,
|
||||
CopyOtp,
|
||||
ImportOtp,
|
||||
ShowOtpUri,
|
||||
CopyOtpUri,
|
||||
ShowOtpQr,
|
||||
RemoveOtp,
|
||||
GitStatus,
|
||||
GitPull,
|
||||
GitPush,
|
||||
@@ -72,6 +79,13 @@ impl UiAction {
|
||||
Self::CopyEntry => "copy-entry",
|
||||
Self::DeleteEntry => "delete-entry",
|
||||
Self::ToggleReveal => "toggle-reveal",
|
||||
Self::GenerateOtp => "generate-otp",
|
||||
Self::CopyOtp => "copy-otp",
|
||||
Self::ImportOtp => "import-otp",
|
||||
Self::ShowOtpUri => "show-otp-uri",
|
||||
Self::CopyOtpUri => "copy-otp-uri",
|
||||
Self::ShowOtpQr => "show-otp-qr",
|
||||
Self::RemoveOtp => "remove-otp",
|
||||
Self::GitStatus => "git-status",
|
||||
Self::GitPull => "git-pull",
|
||||
Self::GitPush => "git-push",
|
||||
@@ -135,6 +149,7 @@ pub struct ActionContext {
|
||||
pub focused_field: bool,
|
||||
pub focused_sensitive: bool,
|
||||
pub focused_generatable: bool,
|
||||
pub focused_otp: bool,
|
||||
pub entry_path: bool,
|
||||
pub selected_object: bool,
|
||||
pub git_running: bool,
|
||||
@@ -237,6 +252,23 @@ pub const ACTIONS: &[ActionSpec] = &[
|
||||
"Reveal or Hide Field",
|
||||
None,
|
||||
),
|
||||
spec(
|
||||
UiAction::GenerateOtp,
|
||||
MenuGroup::Entry,
|
||||
"Generate OTP Code",
|
||||
None,
|
||||
),
|
||||
spec(UiAction::CopyOtp, MenuGroup::Entry, "Copy OTP Code", None),
|
||||
spec(
|
||||
UiAction::ImportOtp,
|
||||
MenuGroup::Entry,
|
||||
"Import OTP URI or QR…",
|
||||
None,
|
||||
),
|
||||
spec(UiAction::ShowOtpUri, MenuGroup::Entry, "Show OTP URI", None),
|
||||
spec(UiAction::CopyOtpUri, MenuGroup::Entry, "Copy OTP URI", None),
|
||||
spec(UiAction::ShowOtpQr, MenuGroup::Entry, "Show OTP QR", None),
|
||||
spec(UiAction::RemoveOtp, MenuGroup::Entry, "Remove OTP", None),
|
||||
spec(UiAction::GitStatus, MenuGroup::Tools, "Git Status…", None),
|
||||
spec(UiAction::GitPull, MenuGroup::Tools, "Pull", None),
|
||||
spec(UiAction::GitPush, MenuGroup::Tools, "Push", None),
|
||||
@@ -378,6 +410,28 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
||||
&& !context.switching_vault
|
||||
&& context.focused_sensitive
|
||||
}
|
||||
UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp => {
|
||||
context.unlocked
|
||||
&& context.document_open
|
||||
&& !context.editing
|
||||
&& context.focused_otp
|
||||
&& !context.saving
|
||||
&& !context.switching_vault
|
||||
&& !context.modal_open
|
||||
}
|
||||
UiAction::ImportOtp => {
|
||||
context.storage_ready
|
||||
&& context.unlocked
|
||||
&& !context.dirty
|
||||
&& !context.saving
|
||||
&& !context.switching_vault
|
||||
&& !context.modal_open
|
||||
}
|
||||
UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync => {
|
||||
context.storage_ready
|
||||
&& !context.saving
|
||||
@@ -497,6 +551,58 @@ pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'sta
|
||||
UiAction::ToggleReveal if !context.document_open => "Open an entry first",
|
||||
UiAction::ToggleReveal if !context.focused_sensitive => "Select a sensitive field first",
|
||||
UiAction::ToggleReveal => "Wait for vault validation",
|
||||
UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp
|
||||
if !context.unlocked =>
|
||||
{
|
||||
"Unlock an entry first"
|
||||
}
|
||||
UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp
|
||||
if !context.document_open =>
|
||||
{
|
||||
"Open an entry first"
|
||||
}
|
||||
UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp
|
||||
if !context.focused_otp =>
|
||||
{
|
||||
"Select an OTP URI field first"
|
||||
}
|
||||
UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp
|
||||
if context.editing =>
|
||||
{
|
||||
"Finish editing the entry first"
|
||||
}
|
||||
UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp => "Close the current screen first",
|
||||
UiAction::ImportOtp if !context.storage_ready => "Shared configuration is unavailable",
|
||||
UiAction::ImportOtp if !context.unlocked => "Unlock the password store first",
|
||||
UiAction::ImportOtp if context.dirty => "Save or discard the current draft first",
|
||||
UiAction::ImportOtp if context.saving => "Wait for the active save",
|
||||
UiAction::ImportOtp if context.switching_vault => "Wait for vault validation",
|
||||
UiAction::ImportOtp => "Close the current screen first",
|
||||
UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync
|
||||
if !context.storage_ready =>
|
||||
{
|
||||
@@ -561,6 +667,13 @@ pub const fn aliases(action: UiAction) -> &'static [&'static str] {
|
||||
UiAction::CopyEntry => &["duplicate entry", "copy folder", "pass cp"],
|
||||
UiAction::DeleteEntry => &["remove", "rm", "delete folder"],
|
||||
UiAction::ToggleReveal => &["show password", "hide password", "reveal field"],
|
||||
UiAction::GenerateOtp => &["totp", "hotp", "one time password"],
|
||||
UiAction::CopyOtp => &["copy totp", "copy hotp", "otp clipboard"],
|
||||
UiAction::ImportOtp => &["add otp", "scan qr", "import otpauth"],
|
||||
UiAction::ShowOtpUri => &["show otpauth", "otp secret"],
|
||||
UiAction::CopyOtpUri => &["copy otpauth", "copy otp secret"],
|
||||
UiAction::ShowOtpQr => &["otp qr", "export otp"],
|
||||
UiAction::RemoveOtp => &["delete otp", "remove otpauth"],
|
||||
UiAction::GitStatus => &["repository status", "git history"],
|
||||
UiAction::GitPull => &["fetch", "download changes"],
|
||||
UiAction::GitPush => &["upload changes"],
|
||||
@@ -623,6 +736,7 @@ mod tests {
|
||||
focused_field: true,
|
||||
focused_sensitive: true,
|
||||
focused_generatable: true,
|
||||
focused_otp: true,
|
||||
entry_path: true,
|
||||
selected_object: true,
|
||||
git_running: false,
|
||||
@@ -672,6 +786,17 @@ mod tests {
|
||||
assert!(enabled(UiAction::InitializeStore, viewing));
|
||||
assert!(enabled(UiAction::NewFolder, viewing));
|
||||
assert!(enabled(UiAction::NewEntry, viewing));
|
||||
for action in [
|
||||
UiAction::GenerateOtp,
|
||||
UiAction::CopyOtp,
|
||||
UiAction::ShowOtpUri,
|
||||
UiAction::CopyOtpUri,
|
||||
UiAction::ShowOtpQr,
|
||||
UiAction::RemoveOtp,
|
||||
] {
|
||||
assert!(enabled(action, viewing), "{action:?}");
|
||||
}
|
||||
assert!(enabled(UiAction::ImportOtp, viewing));
|
||||
|
||||
let locked = ActionContext {
|
||||
unlocked: false,
|
||||
@@ -683,6 +808,13 @@ mod tests {
|
||||
UiAction::CopyEditedField,
|
||||
UiAction::ToggleReveal,
|
||||
UiAction::GeneratePassword,
|
||||
UiAction::GenerateOtp,
|
||||
UiAction::CopyOtp,
|
||||
UiAction::ImportOtp,
|
||||
UiAction::ShowOtpUri,
|
||||
UiAction::CopyOtpUri,
|
||||
UiAction::ShowOtpQr,
|
||||
UiAction::RemoveOtp,
|
||||
UiAction::Lock,
|
||||
] {
|
||||
assert!(!enabled(action, locked));
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ironstorage::repository::SecretBytes;
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||
let mut dialog = rfd::AsyncFileDialog::new().set_title("Open Password Store");
|
||||
@@ -14,6 +16,22 @@ pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, St
|
||||
.map(|folder| folder.path().to_owned()))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
pub async fn pick_qr_image() -> Result<Option<SecretBytes>, String> {
|
||||
let selected = rfd::AsyncFileDialog::new()
|
||||
.set_title("Import OTP QR Image")
|
||||
.add_filter("Image", &["png", "jpg", "jpeg", "gif"])
|
||||
.pick_file()
|
||||
.await;
|
||||
selected
|
||||
.map(|file| {
|
||||
std::fs::read(file.path())
|
||||
.map(SecretBytes::new)
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||
use ashpd::{
|
||||
@@ -47,6 +65,37 @@ pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, St
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn pick_qr_image() -> Result<Option<SecretBytes>, String> {
|
||||
use ashpd::{
|
||||
PortalError,
|
||||
desktop::{file_chooser::SelectedFiles, request::ResponseError},
|
||||
};
|
||||
|
||||
let response = SelectedFiles::open_file()
|
||||
.title("Import OTP QR Image")
|
||||
.accept_label("Import")
|
||||
.modal(true)
|
||||
.multiple(false)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|request| request.response());
|
||||
let selected = match response {
|
||||
Ok(selected) => selected,
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled))
|
||||
| Err(ashpd::Error::Portal(PortalError::Cancelled(_))) => return Ok(None),
|
||||
Err(error) => return Err(error.to_string()),
|
||||
};
|
||||
let Some(uri) = selected.uris().first() else {
|
||||
return Err("file portal returned no selection".to_owned());
|
||||
};
|
||||
let path = file_uri_path(uri.as_str())?;
|
||||
std::fs::read(path)
|
||||
.map(SecretBytes::new)
|
||||
.map(Some)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn file_uri_path(uri: &str) -> Result<PathBuf, String> {
|
||||
let uri = url::Url::parse(uri).map_err(|_| "folder portal returned an invalid URI")?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -145,6 +145,13 @@ fn accelerator(action: UiAction) -> Option<Accelerator> {
|
||||
| UiAction::CopyEntry
|
||||
| UiAction::DeleteEntry
|
||||
| UiAction::ToggleReveal
|
||||
| UiAction::GenerateOtp
|
||||
| UiAction::CopyOtp
|
||||
| UiAction::ImportOtp
|
||||
| UiAction::ShowOtpUri
|
||||
| UiAction::CopyOtpUri
|
||||
| UiAction::ShowOtpQr
|
||||
| UiAction::RemoveOtp
|
||||
| UiAction::GitStatus
|
||||
| UiAction::GitPull
|
||||
| UiAction::GitPush
|
||||
|
||||
Reference in New Issue
Block a user