Show OTP codes above desktop entry fields
This commit is contained in:
@@ -26,8 +26,8 @@ use iced::{
|
|||||||
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size,
|
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size,
|
||||||
Subscription, Task, Theme, event, keyboard, mouse, time, touch,
|
Subscription, Task, Theme, event, keyboard, mouse, time, touch,
|
||||||
widget::{
|
widget::{
|
||||||
button, canvas, column, container, mouse_area, pane_grid, row, scrollable, text,
|
button, canvas, column, container, mouse_area, pane_grid, progress_bar, row, scrollable,
|
||||||
text_editor, text_input, tooltip,
|
text, text_editor, text_input, tooltip,
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
};
|
};
|
||||||
@@ -5341,11 +5341,70 @@ fn otp_code_text(display: &OtpDisplay) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matching_otp_display<'a>(app: &'a App, entry: &str) -> Option<&'a OtpDisplay> {
|
||||||
|
app.sensitive
|
||||||
|
.otp
|
||||||
|
.as_ref()
|
||||||
|
.filter(|display| display.entry == entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn otp_progress(display: &OtpDisplay) -> Option<(u64, u64)> {
|
||||||
|
display.validity.period().map(|period| {
|
||||||
|
(
|
||||||
|
display
|
||||||
|
.remaining_at(display.observed_at)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.min(period),
|
||||||
|
period.max(1),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message> {
|
fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message> {
|
||||||
let entry = editor.entry();
|
let entry = editor.entry();
|
||||||
let mut rows =
|
let mut rows = column![].spacing(8);
|
||||||
column![text("Up/Down/Home/End select fields · ⌘C copies the selected value").size(12),]
|
if let Some(display) = matching_otp_display(app, &entry) {
|
||||||
.spacing(8);
|
let mut code = column![
|
||||||
|
text(match display.metadata.kind() {
|
||||||
|
OtpKind::Totp => "Current one-time password",
|
||||||
|
OtpKind::Hotp => "Generated one-time password",
|
||||||
|
})
|
||||||
|
.size(13),
|
||||||
|
text(otp_code_text(display)).size(42),
|
||||||
|
]
|
||||||
|
.align_x(iced::Alignment::Center)
|
||||||
|
.spacing(2);
|
||||||
|
if let Some((remaining, period)) = otp_progress(display) {
|
||||||
|
code = code.push(text(format!("{remaining}s remaining"))).push(
|
||||||
|
progress_bar(0.0..=period as f32, remaining as f32).girth(Length::Fixed(6.0)),
|
||||||
|
);
|
||||||
|
} else if let Some(counter) = display.validity.counter() {
|
||||||
|
code = code.push(text(format!("HOTP counter {counter}")));
|
||||||
|
}
|
||||||
|
rows = rows.push(
|
||||||
|
container(code)
|
||||||
|
.padding([10, 12])
|
||||||
|
.width(Length::Fill)
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.style(entry_value_style),
|
||||||
|
);
|
||||||
|
} else if app.otp_pending
|
||||||
|
&& editor
|
||||||
|
.document()
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.metadata().otp().is_some())
|
||||||
|
{
|
||||||
|
rows = rows.push(
|
||||||
|
container(text("Generating one-time password…").size(15))
|
||||||
|
.padding(10)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.style(entry_value_style),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows =
|
||||||
|
rows.push(text("Up/Down/Home/End select fields · ⌘C copies the selected value").size(12));
|
||||||
|
|
||||||
for field in editor.document().display_fields() {
|
for field in editor.document().display_fields() {
|
||||||
let id = field.id();
|
let id = field.id();
|
||||||
@@ -5392,34 +5451,6 @@ fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message
|
|||||||
|| format!("counter {}", otp.counter().unwrap_or_default()),
|
|| format!("counter {}", otp.counter().unwrap_or_default()),
|
||||||
|period| format!("{period}s period"),
|
|period| format!("{period}s period"),
|
||||||
);
|
);
|
||||||
let code = app
|
|
||||||
.sensitive
|
|
||||||
.otp
|
|
||||||
.as_ref()
|
|
||||||
.filter(|display| display.entry == entry);
|
|
||||||
field_view = field_view.push(match code {
|
|
||||||
Some(display) => column![
|
|
||||||
text(otp_code_text(display)).size(28),
|
|
||||||
text(display.remaining_at(display.observed_at).map_or_else(
|
|
||||||
|| format!(
|
|
||||||
"HOTP counter {}",
|
|
||||||
display.validity.counter().unwrap_or_default()
|
|
||||||
),
|
|
||||||
|remaining| format!("Valid for {remaining}s"),
|
|
||||||
))
|
|
||||||
.size(12),
|
|
||||||
]
|
|
||||||
.spacing(1),
|
|
||||||
None if app.otp_pending => column![text("Generating one-time password…").size(13)],
|
|
||||||
None => column![
|
|
||||||
text(if otp.kind() == OtpKind::Hotp {
|
|
||||||
"Generate the next HOTP code to advance its counter."
|
|
||||||
} else {
|
|
||||||
"One-time password unavailable. Refresh to retry."
|
|
||||||
})
|
|
||||||
.size(13)
|
|
||||||
],
|
|
||||||
});
|
|
||||||
field_view = field_view.push(
|
field_view = field_view.push(
|
||||||
text(format!(
|
text(format!(
|
||||||
"{:?} · {} · {} · {:?} · {} digits · {cadence}",
|
"{:?} · {} · {} · {:?} · {} digits · {cadence}",
|
||||||
@@ -6783,6 +6814,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(display.remaining_at(59), Some(1));
|
assert_eq!(display.remaining_at(59), Some(1));
|
||||||
assert_eq!(display.remaining_at(60), Some(0));
|
assert_eq!(display.remaining_at(60), Some(0));
|
||||||
|
assert_eq!(otp_progress(&display), Some((1, 30)));
|
||||||
|
assert_eq!(otp_code_text(&display), "9428 7082");
|
||||||
|
|
||||||
let mut hotp_document = storage
|
let mut hotp_document = storage
|
||||||
.create_document("otp/rfc4226", &mut provider)
|
.create_document("otp/rfc4226", &mut provider)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ requires every registered action ID to remain present in this document.
|
|||||||
| Base pass: `grep` | `search-contents` (Edit) | Authenticated decrypted-search form and typed result activation | Yes |
|
| Base pass: `grep` | `search-contents` (Edit) | Authenticated decrypted-search form and typed result activation | Yes |
|
||||||
| Base pass: `mv`/`rename`, `cp`/`copy`, `rm`/`remove` | `move-entry`, `copy-entry`, `delete-entry` (Entry) | Sidebar context controls and validated mutation forms; delete is confirmed | Yes |
|
| Base pass: `mv`/`rename`, `cp`/`copy`, `rm`/`remove` | `move-entry`, `copy-entry`, `delete-entry` (Entry) | Sidebar context controls and validated mutation forms; delete is confirmed | Yes |
|
||||||
| KeePass migration: additive KDBX import and quick add | `import-kdbx` (Tools) | Native file/key picker, protected password input, explicit confirmation, and storage-owned tree refresh | Yes |
|
| KeePass migration: additive KDBX import and quick add | `import-kdbx` (Tools) | Native file/key picker, protected password input, explicit confirmation, and storage-owned tree refresh | Yes |
|
||||||
| Pass OTP: code/show and timed copy | `generate-otp`, `copy-otp` (Entry) | OTP panel shows typed metadata, code, validity, and HOTP confirmation | Yes |
|
| Pass OTP: code/show and timed copy | `generate-otp`, `copy-otp` (Entry) | Unlocked entry header shows the large code and storage-period countdown bar; the OTP field retains metadata/actions and HOTP confirmation | Yes |
|
||||||
| Pass OTP: insert/add/append and validate | `import-otp` (Entry) | URI/QR import form; storage validates, replaces, and commits | Yes |
|
| Pass OTP: insert/add/append and validate | `import-otp` (Entry) | URI/QR import form; storage validates, replaces, and commits | Yes |
|
||||||
| Pass OTP: URI terminal/clipboard/QR | `show-otp-uri`, `copy-otp-uri`, `show-otp-qr` (Entry) | Explicit secret view, timed copy, and storage-provided QR matrix | Yes |
|
| Pass OTP: URI terminal/clipboard/QR | `show-otp-uri`, `copy-otp-uri`, `show-otp-qr` (Entry) | Explicit secret view, timed copy, and storage-provided QR matrix | Yes |
|
||||||
| Pass OTP: remove | `remove-otp` (Entry) | Explicit permanent-removal confirmation | Yes |
|
| Pass OTP: remove | `remove-otp` (Entry) | Explicit permanent-removal confirmation | Yes |
|
||||||
|
|||||||
Reference in New Issue
Block a user