Present desktop OTP and sensitive outputs
This commit is contained in:
137
Cargo.lock
generated
137
Cargo.lock
generated
@@ -806,6 +806,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.12.1"
|
version = "1.12.1"
|
||||||
@@ -1194,6 +1200,12 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color_quant"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -2172,6 +2184,12 @@ dependencies = [
|
|||||||
"zlib-rs",
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float_next_after"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -2556,6 +2574,16 @@ dependencies = [
|
|||||||
"polyval",
|
"polyval",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gif"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||||
|
dependencies = [
|
||||||
|
"color_quant",
|
||||||
|
"weezl",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio"
|
name = "gio"
|
||||||
version = "0.18.4"
|
version = "0.18.4"
|
||||||
@@ -3971,6 +3999,7 @@ dependencies = [
|
|||||||
"iced_core",
|
"iced_core",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
"log",
|
"log",
|
||||||
|
"lyon_path",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.19",
|
||||||
@@ -4045,6 +4074,7 @@ dependencies = [
|
|||||||
"iced_debug",
|
"iced_debug",
|
||||||
"iced_graphics",
|
"iced_graphics",
|
||||||
"log",
|
"log",
|
||||||
|
"lyon",
|
||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.19",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
@@ -4201,6 +4231,23 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.25.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder-lite",
|
||||||
|
"color_quant",
|
||||||
|
"gif",
|
||||||
|
"moxcms",
|
||||||
|
"num-traits",
|
||||||
|
"png",
|
||||||
|
"zune-core",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@@ -4289,6 +4336,7 @@ dependencies = [
|
|||||||
"gix-config",
|
"gix-config",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
"image",
|
||||||
"keyring-core",
|
"keyring-core",
|
||||||
"nix 0.31.3",
|
"nix 0.31.3",
|
||||||
"pgp",
|
"pgp",
|
||||||
@@ -4745,6 +4793,58 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7"
|
||||||
|
dependencies = [
|
||||||
|
"lyon_algorithms",
|
||||||
|
"lyon_tessellation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_algorithms"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31"
|
||||||
|
dependencies = [
|
||||||
|
"lyon_path",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_geom"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"euclid",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_path"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e"
|
||||||
|
dependencies = [
|
||||||
|
"lyon_geom",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_tessellation"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552"
|
||||||
|
dependencies = [
|
||||||
|
"float_next_after",
|
||||||
|
"lyon_path",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac_address"
|
name = "mac_address"
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
@@ -4859,6 +4959,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moxcms"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.19.3"
|
version = "0.19.3"
|
||||||
@@ -6169,6 +6279,12 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qrcode"
|
name = "qrcode"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@@ -8641,6 +8757,12 @@ dependencies = [
|
|||||||
"nom 7.1.3",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "weezl"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wezterm-bidi"
|
name = "wezterm-bidi"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -9610,6 +9732,21 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-core"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-jpeg"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||||
|
dependencies = [
|
||||||
|
"zune-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.13.1"
|
version = "5.13.1"
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ flate2 = "1.1"
|
|||||||
gix = { version = "0.86", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "index", "merge", "revision", "sha1", "tree-editor"] }
|
gix = { version = "0.86", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "index", "merge", "revision", "sha1", "tree-editor"] }
|
||||||
gix-config = "0.59"
|
gix-config = "0.59"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
iced = { version = "0.14", features = ["tokio"] }
|
iced = { version = "0.14", features = ["canvas", "tokio"] }
|
||||||
|
image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
|
||||||
ironstorage = { path = "crates/storage" }
|
ironstorage = { path = "crates/storage" }
|
||||||
keyring-core = "1.0"
|
keyring-core = "1.0"
|
||||||
muda = "0.19"
|
muda = "0.19"
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ pub enum UiAction {
|
|||||||
CopyEntry,
|
CopyEntry,
|
||||||
DeleteEntry,
|
DeleteEntry,
|
||||||
ToggleReveal,
|
ToggleReveal,
|
||||||
|
GenerateOtp,
|
||||||
|
CopyOtp,
|
||||||
|
ImportOtp,
|
||||||
|
ShowOtpUri,
|
||||||
|
CopyOtpUri,
|
||||||
|
ShowOtpQr,
|
||||||
|
RemoveOtp,
|
||||||
GitStatus,
|
GitStatus,
|
||||||
GitPull,
|
GitPull,
|
||||||
GitPush,
|
GitPush,
|
||||||
@@ -72,6 +79,13 @@ impl UiAction {
|
|||||||
Self::CopyEntry => "copy-entry",
|
Self::CopyEntry => "copy-entry",
|
||||||
Self::DeleteEntry => "delete-entry",
|
Self::DeleteEntry => "delete-entry",
|
||||||
Self::ToggleReveal => "toggle-reveal",
|
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::GitStatus => "git-status",
|
||||||
Self::GitPull => "git-pull",
|
Self::GitPull => "git-pull",
|
||||||
Self::GitPush => "git-push",
|
Self::GitPush => "git-push",
|
||||||
@@ -135,6 +149,7 @@ pub struct ActionContext {
|
|||||||
pub focused_field: bool,
|
pub focused_field: bool,
|
||||||
pub focused_sensitive: bool,
|
pub focused_sensitive: bool,
|
||||||
pub focused_generatable: bool,
|
pub focused_generatable: bool,
|
||||||
|
pub focused_otp: bool,
|
||||||
pub entry_path: bool,
|
pub entry_path: bool,
|
||||||
pub selected_object: bool,
|
pub selected_object: bool,
|
||||||
pub git_running: bool,
|
pub git_running: bool,
|
||||||
@@ -237,6 +252,23 @@ pub const ACTIONS: &[ActionSpec] = &[
|
|||||||
"Reveal or Hide Field",
|
"Reveal or Hide Field",
|
||||||
None,
|
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::GitStatus, MenuGroup::Tools, "Git Status…", None),
|
||||||
spec(UiAction::GitPull, MenuGroup::Tools, "Pull", None),
|
spec(UiAction::GitPull, MenuGroup::Tools, "Pull", None),
|
||||||
spec(UiAction::GitPush, MenuGroup::Tools, "Push", None),
|
spec(UiAction::GitPush, MenuGroup::Tools, "Push", None),
|
||||||
@@ -378,6 +410,28 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
|||||||
&& !context.switching_vault
|
&& !context.switching_vault
|
||||||
&& context.focused_sensitive
|
&& 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 => {
|
UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync => {
|
||||||
context.storage_ready
|
context.storage_ready
|
||||||
&& !context.saving
|
&& !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.document_open => "Open an entry first",
|
||||||
UiAction::ToggleReveal if !context.focused_sensitive => "Select a sensitive field first",
|
UiAction::ToggleReveal if !context.focused_sensitive => "Select a sensitive field first",
|
||||||
UiAction::ToggleReveal => "Wait for vault validation",
|
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
|
UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync
|
||||||
if !context.storage_ready =>
|
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::CopyEntry => &["duplicate entry", "copy folder", "pass cp"],
|
||||||
UiAction::DeleteEntry => &["remove", "rm", "delete folder"],
|
UiAction::DeleteEntry => &["remove", "rm", "delete folder"],
|
||||||
UiAction::ToggleReveal => &["show password", "hide password", "reveal field"],
|
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::GitStatus => &["repository status", "git history"],
|
||||||
UiAction::GitPull => &["fetch", "download changes"],
|
UiAction::GitPull => &["fetch", "download changes"],
|
||||||
UiAction::GitPush => &["upload changes"],
|
UiAction::GitPush => &["upload changes"],
|
||||||
@@ -623,6 +736,7 @@ mod tests {
|
|||||||
focused_field: true,
|
focused_field: true,
|
||||||
focused_sensitive: true,
|
focused_sensitive: true,
|
||||||
focused_generatable: true,
|
focused_generatable: true,
|
||||||
|
focused_otp: true,
|
||||||
entry_path: true,
|
entry_path: true,
|
||||||
selected_object: true,
|
selected_object: true,
|
||||||
git_running: false,
|
git_running: false,
|
||||||
@@ -672,6 +786,17 @@ mod tests {
|
|||||||
assert!(enabled(UiAction::InitializeStore, viewing));
|
assert!(enabled(UiAction::InitializeStore, viewing));
|
||||||
assert!(enabled(UiAction::NewFolder, viewing));
|
assert!(enabled(UiAction::NewFolder, viewing));
|
||||||
assert!(enabled(UiAction::NewEntry, 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 {
|
let locked = ActionContext {
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
@@ -683,6 +808,13 @@ mod tests {
|
|||||||
UiAction::CopyEditedField,
|
UiAction::CopyEditedField,
|
||||||
UiAction::ToggleReveal,
|
UiAction::ToggleReveal,
|
||||||
UiAction::GeneratePassword,
|
UiAction::GeneratePassword,
|
||||||
|
UiAction::GenerateOtp,
|
||||||
|
UiAction::CopyOtp,
|
||||||
|
UiAction::ImportOtp,
|
||||||
|
UiAction::ShowOtpUri,
|
||||||
|
UiAction::CopyOtpUri,
|
||||||
|
UiAction::ShowOtpQr,
|
||||||
|
UiAction::RemoveOtp,
|
||||||
UiAction::Lock,
|
UiAction::Lock,
|
||||||
] {
|
] {
|
||||||
assert!(!enabled(action, locked));
|
assert!(!enabled(action, locked));
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use ironstorage::repository::SecretBytes;
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||||
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||||
let mut dialog = rfd::AsyncFileDialog::new().set_title("Open Password Store");
|
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()))
|
.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")]
|
#[cfg(target_os = "linux")]
|
||||||
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||||
use ashpd::{
|
use ashpd::{
|
||||||
@@ -47,6 +65,37 @@ pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, St
|
|||||||
.map(Some)
|
.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")]
|
#[cfg(target_os = "linux")]
|
||||||
fn file_uri_path(uri: &str) -> Result<PathBuf, String> {
|
fn file_uri_path(uri: &str) -> Result<PathBuf, String> {
|
||||||
let uri = url::Url::parse(uri).map_err(|_| "folder portal returned an invalid URI")?;
|
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::CopyEntry
|
||||||
| UiAction::DeleteEntry
|
| UiAction::DeleteEntry
|
||||||
| UiAction::ToggleReveal
|
| UiAction::ToggleReveal
|
||||||
|
| UiAction::GenerateOtp
|
||||||
|
| UiAction::CopyOtp
|
||||||
|
| UiAction::ImportOtp
|
||||||
|
| UiAction::ShowOtpUri
|
||||||
|
| UiAction::CopyOtpUri
|
||||||
|
| UiAction::ShowOtpQr
|
||||||
|
| UiAction::RemoveOtp
|
||||||
| UiAction::GitStatus
|
| UiAction::GitStatus
|
||||||
| UiAction::GitPull
|
| UiAction::GitPull
|
||||||
| UiAction::GitPush
|
| UiAction::GitPush
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ flate2.workspace = true
|
|||||||
gix.workspace = true
|
gix.workspace = true
|
||||||
gix-config.workspace = true
|
gix-config.workspace = true
|
||||||
hmac.workspace = true
|
hmac.workspace = true
|
||||||
|
image.workspace = true
|
||||||
keyring-core.workspace = true
|
keyring-core.workspace = true
|
||||||
pgp.workspace = true
|
pgp.workspace = true
|
||||||
qrcode.workspace = true
|
qrcode.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
rqrr.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
sha1.workspace = true
|
sha1.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
@@ -49,6 +51,5 @@ arboard.workspace = true
|
|||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
nix = { version = "0.31", features = ["fs"] }
|
nix = { version = "0.31", features = ["fs"] }
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
rqrr.workspace = true
|
|
||||||
smallvec = "1.15"
|
smallvec = "1.15"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ use crate::{
|
|||||||
authentication::{
|
authentication::{
|
||||||
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
|
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
|
||||||
},
|
},
|
||||||
command::{CopyRequest, FindRequest, GrepRequest, InitRequest, MoveRequest, RemoveRequest},
|
command::{
|
||||||
|
CopyRequest, FindRequest, GrepRequest, InitRequest, MoveRequest, OtpInputSource,
|
||||||
|
OtpInsertRequest, RemoveRequest,
|
||||||
|
},
|
||||||
config::{Config, ConfigSettings, EditorCommand},
|
config::{Config, ConfigSettings, EditorCommand},
|
||||||
crypto::{KeyInfo, KeyStore, SecretProvider},
|
crypto::{KeyInfo, KeyStore, SecretProvider},
|
||||||
document::{DocumentError, EntryDocument, EntryDocumentService},
|
document::{
|
||||||
|
DocumentError, EntryDocument, EntryDocumentService, EntryFieldDraft, EntryFieldId,
|
||||||
|
EntryFieldKind,
|
||||||
|
},
|
||||||
git::{
|
git::{
|
||||||
AutomaticEntryCommitter, AutomaticPolicyCommitter, AutomaticTreeCommitter,
|
AutomaticEntryCommitter, AutomaticPolicyCommitter, AutomaticTreeCommitter,
|
||||||
EmbeddedFetchTransport, GitConflict, GitConflictResolution, GitError, GitIdentity,
|
EmbeddedFetchTransport, GitConflict, GitConflictResolution, GitError, GitIdentity,
|
||||||
@@ -17,13 +23,14 @@ use crate::{
|
|||||||
PushOutcome, ReqwestGitTransport,
|
PushOutcome, ReqwestGitTransport,
|
||||||
},
|
},
|
||||||
mutation::{MutationOutcome, TreeMutator},
|
mutation::{MutationOutcome, TreeMutator},
|
||||||
presentation::ClipboardTimeout,
|
otp::{OtpAlgorithm, OtpCodeValidity, OtpInput, OtpKind, OtpService, OtpUri},
|
||||||
|
presentation::{ClipboardTimeout, QrMatrix},
|
||||||
read::{FindResults, GrepResults, TreeModel, VaultReader},
|
read::{FindResults, GrepResults, TreeModel, VaultReader},
|
||||||
recipient::{
|
recipient::{
|
||||||
PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
|
PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
|
||||||
RecipientPolicyOutcome,
|
RecipientPolicyOutcome,
|
||||||
},
|
},
|
||||||
repository::{DirectoryPath, Repository},
|
repository::{DirectoryPath, Repository, SecretBytes},
|
||||||
secret_store::SecretProtectionPolicy,
|
secret_store::SecretProtectionPolicy,
|
||||||
write::{OverwriteDecision, VaultWriter, WriteError, WriteOutcome},
|
write::{OverwriteDecision, VaultWriter, WriteError, WriteOutcome},
|
||||||
};
|
};
|
||||||
@@ -42,6 +49,7 @@ pub enum DesktopErrorKind {
|
|||||||
MissingDefaultKey,
|
MissingDefaultKey,
|
||||||
EntryExists,
|
EntryExists,
|
||||||
Mutation,
|
Mutation,
|
||||||
|
Otp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -159,6 +167,112 @@ pub struct DesktopGitResult {
|
|||||||
tree: Option<TreeModel>,
|
tree: Option<TreeModel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct DesktopOtpMetadata {
|
||||||
|
kind: OtpKind,
|
||||||
|
issuer: Option<String>,
|
||||||
|
account: String,
|
||||||
|
algorithm: OtpAlgorithm,
|
||||||
|
digits: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopOtpMetadata {
|
||||||
|
pub fn kind(&self) -> OtpKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn issuer(&self) -> Option<&str> {
|
||||||
|
self.issuer.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn account(&self) -> &str {
|
||||||
|
&self.account
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn algorithm(&self) -> OtpAlgorithm {
|
||||||
|
self.algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn digits(&self) -> u32 {
|
||||||
|
self.digits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DesktopOtpCode {
|
||||||
|
code: SecretBytes,
|
||||||
|
validity: OtpCodeValidity,
|
||||||
|
metadata: DesktopOtpMetadata,
|
||||||
|
tree: Option<TreeModel>,
|
||||||
|
document: Option<EntryDocument>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopOtpCode {
|
||||||
|
pub fn code(&self) -> &SecretBytes {
|
||||||
|
&self.code
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validity(&self) -> OtpCodeValidity {
|
||||||
|
self.validity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata(&self) -> &DesktopOtpMetadata {
|
||||||
|
&self.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_parts(
|
||||||
|
self,
|
||||||
|
) -> (
|
||||||
|
SecretBytes,
|
||||||
|
OtpCodeValidity,
|
||||||
|
DesktopOtpMetadata,
|
||||||
|
Option<TreeModel>,
|
||||||
|
Option<EntryDocument>,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
self.code,
|
||||||
|
self.validity,
|
||||||
|
self.metadata,
|
||||||
|
self.tree,
|
||||||
|
self.document,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DesktopOtpUri {
|
||||||
|
payload: SecretBytes,
|
||||||
|
matrix: Option<QrMatrix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopOtpUri {
|
||||||
|
pub fn payload(&self) -> &SecretBytes {
|
||||||
|
&self.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matrix(&self) -> Option<&QrMatrix> {
|
||||||
|
self.matrix.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_parts(self) -> (SecretBytes, Option<QrMatrix>) {
|
||||||
|
(self.payload, self.matrix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DesktopOtpMutation {
|
||||||
|
entry: String,
|
||||||
|
document: EntryDocument,
|
||||||
|
tree: TreeModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopOtpMutation {
|
||||||
|
pub fn entry(&self) -> &str {
|
||||||
|
&self.entry
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_parts(self) -> (String, EntryDocument, TreeModel) {
|
||||||
|
(self.entry, self.document, self.tree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DesktopGitResult {
|
impl DesktopGitResult {
|
||||||
pub fn outcome(&self) -> &DesktopGitOutcome {
|
pub fn outcome(&self) -> &DesktopGitOutcome {
|
||||||
&self.outcome
|
&self.outcome
|
||||||
@@ -420,6 +534,263 @@ impl DesktopStorage {
|
|||||||
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn otp_code_active(
|
||||||
|
&self,
|
||||||
|
handle: &NativeAuthenticationHandle,
|
||||||
|
entry: &str,
|
||||||
|
unix_seconds: u64,
|
||||||
|
confirm_hotp: bool,
|
||||||
|
) -> Result<DesktopOtpCode, DesktopError> {
|
||||||
|
handle
|
||||||
|
.ensure_active()
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||||
|
let mut provider = handle.clone();
|
||||||
|
self.otp_code(entry, unix_seconds, confirm_hotp, &mut provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn otp_code(
|
||||||
|
&self,
|
||||||
|
entry: &str,
|
||||||
|
unix_seconds: u64,
|
||||||
|
confirm_hotp: bool,
|
||||||
|
provider: &mut impl SecretProvider,
|
||||||
|
) -> Result<DesktopOtpCode, DesktopError> {
|
||||||
|
let repository = self.repository()?;
|
||||||
|
let keys = self.keys()?;
|
||||||
|
let service = OtpService::new(&repository, &keys);
|
||||||
|
let uri = service
|
||||||
|
.uri(entry, provider)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
if uri.kind() == OtpKind::Hotp && !confirm_hotp {
|
||||||
|
return Err(DesktopError::new(
|
||||||
|
DesktopErrorKind::Otp,
|
||||||
|
"HOTP generation requires explicit confirmation because it commits the advanced counter",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let metadata = DesktopOtpMetadata {
|
||||||
|
kind: uri.kind(),
|
||||||
|
issuer: uri.issuer().map(str::to_owned),
|
||||||
|
account: uri.account().to_owned(),
|
||||||
|
algorithm: uri.algorithm(),
|
||||||
|
digits: uri.digits(),
|
||||||
|
};
|
||||||
|
let outcome = service
|
||||||
|
.code_automatic(entry, unix_seconds, None, provider)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
let validity = outcome.validity();
|
||||||
|
let changed = validity.counter().is_some();
|
||||||
|
let tree = changed.then(|| self.tree()).transpose()?;
|
||||||
|
let document = changed
|
||||||
|
.then(|| {
|
||||||
|
EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, provider)
|
||||||
|
.map_err(DesktopError::document)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
Ok(DesktopOtpCode {
|
||||||
|
code: SecretBytes::new(outcome.code().expose().to_vec()),
|
||||||
|
validity,
|
||||||
|
metadata,
|
||||||
|
tree,
|
||||||
|
document,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn otp_uri_active(
|
||||||
|
&self,
|
||||||
|
handle: &NativeAuthenticationHandle,
|
||||||
|
entry: &str,
|
||||||
|
qr: bool,
|
||||||
|
) -> Result<DesktopOtpUri, DesktopError> {
|
||||||
|
handle
|
||||||
|
.ensure_active()
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||||
|
let mut provider = handle.clone();
|
||||||
|
self.otp_uri(entry, qr, &mut provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn otp_uri(
|
||||||
|
&self,
|
||||||
|
entry: &str,
|
||||||
|
qr: bool,
|
||||||
|
provider: &mut impl SecretProvider,
|
||||||
|
) -> Result<DesktopOtpUri, DesktopError> {
|
||||||
|
let repository = self.repository()?;
|
||||||
|
let keys = self.keys()?;
|
||||||
|
let uri = OtpService::new(&repository, &keys)
|
||||||
|
.uri(entry, provider)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
let payload = SecretBytes::new(uri.encoded().expose().to_vec());
|
||||||
|
let matrix = qr
|
||||||
|
.then(|| QrMatrix::encode(&payload))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
Ok(DesktopOtpUri { payload, matrix })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_otp_active(
|
||||||
|
&self,
|
||||||
|
handle: &NativeAuthenticationHandle,
|
||||||
|
entry: &str,
|
||||||
|
uri: SecretBytes,
|
||||||
|
replace: OverwriteDecision,
|
||||||
|
) -> Result<DesktopOtpMutation, DesktopError> {
|
||||||
|
handle
|
||||||
|
.ensure_active()
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||||
|
let mut provider = handle.clone();
|
||||||
|
self.import_otp(entry, uri, replace, &mut provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_otp_qr_active(
|
||||||
|
&self,
|
||||||
|
handle: &NativeAuthenticationHandle,
|
||||||
|
entry: &str,
|
||||||
|
image: SecretBytes,
|
||||||
|
replace: OverwriteDecision,
|
||||||
|
) -> Result<DesktopOtpMutation, DesktopError> {
|
||||||
|
handle
|
||||||
|
.ensure_active()
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||||
|
let mut provider = handle.clone();
|
||||||
|
self.import_otp_qr(entry, image, replace, &mut provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_otp_qr(
|
||||||
|
&self,
|
||||||
|
entry: &str,
|
||||||
|
image: SecretBytes,
|
||||||
|
replace: OverwriteDecision,
|
||||||
|
provider: &mut impl SecretProvider,
|
||||||
|
) -> Result<DesktopOtpMutation, DesktopError> {
|
||||||
|
let uri = QrMatrix::decode_image(&image)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
self.import_otp(entry, uri, replace, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_otp(
|
||||||
|
&self,
|
||||||
|
entry: &str,
|
||||||
|
uri: SecretBytes,
|
||||||
|
replace: OverwriteDecision,
|
||||||
|
provider: &mut impl SecretProvider,
|
||||||
|
) -> Result<DesktopOtpMutation, DesktopError> {
|
||||||
|
let parsed =
|
||||||
|
OtpUri::parse(uri).map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
let encoded = parsed.encoded().expose().to_vec();
|
||||||
|
let repository = self.repository()?;
|
||||||
|
let keys = self.keys()?;
|
||||||
|
let exists = VaultWriter::new(&repository, &keys)
|
||||||
|
.entry_exists(entry)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
if exists {
|
||||||
|
let mut document = EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, provider)
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
if let Some(field) = otp_field(&document)? {
|
||||||
|
if replace == OverwriteDecision::Decline {
|
||||||
|
return Err(DesktopError::new(
|
||||||
|
DesktopErrorKind::EntryExists,
|
||||||
|
format!("entry already contains an OTP URI: {entry}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
document
|
||||||
|
.replace_field_value(field, encoded.clone())
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
} else {
|
||||||
|
let index = document.fields().len();
|
||||||
|
document
|
||||||
|
.add(
|
||||||
|
index,
|
||||||
|
EntryFieldDraft::otp_uri(encoded.clone())
|
||||||
|
.map_err(DesktopError::document)?,
|
||||||
|
)
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
}
|
||||||
|
self.save_document(&document)?;
|
||||||
|
let document = EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, provider)
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
return Ok(DesktopOtpMutation {
|
||||||
|
entry: entry.to_owned(),
|
||||||
|
document,
|
||||||
|
tree: self.tree()?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let request = OtpInsertRequest {
|
||||||
|
entry: Some(entry.to_owned()),
|
||||||
|
force: false,
|
||||||
|
echo: true,
|
||||||
|
source: OtpInputSource::Uri,
|
||||||
|
};
|
||||||
|
let input = OtpInput::line(encoded)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
let service = OtpService::new(&repository, &keys);
|
||||||
|
let plan = service
|
||||||
|
.prepare_insert(&request, input)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
let mut committer =
|
||||||
|
AutomaticEntryCommitter::for_entry(&repository, entry, GitIdentity::ironstorage())
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
|
||||||
|
service
|
||||||
|
.finish_insert(
|
||||||
|
plan,
|
||||||
|
OverwriteDecision::Allow,
|
||||||
|
OverwriteDecision::Decline,
|
||||||
|
None,
|
||||||
|
&mut committer,
|
||||||
|
)
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
|
||||||
|
let document = EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, provider)
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
Ok(DesktopOtpMutation {
|
||||||
|
entry: entry.to_owned(),
|
||||||
|
document,
|
||||||
|
tree: self.tree()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_otp_active(
|
||||||
|
&self,
|
||||||
|
handle: &NativeAuthenticationHandle,
|
||||||
|
entry: &str,
|
||||||
|
) -> Result<DesktopOtpMutation, DesktopError> {
|
||||||
|
handle
|
||||||
|
.ensure_active()
|
||||||
|
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
|
||||||
|
let mut provider = handle.clone();
|
||||||
|
self.remove_otp(entry, &mut provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_otp(
|
||||||
|
&self,
|
||||||
|
entry: &str,
|
||||||
|
provider: &mut impl SecretProvider,
|
||||||
|
) -> Result<DesktopOtpMutation, DesktopError> {
|
||||||
|
let repository = self.repository()?;
|
||||||
|
let keys = self.keys()?;
|
||||||
|
let mut document = EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, provider)
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
let field = otp_field(&document)?.ok_or_else(|| {
|
||||||
|
DesktopError::new(
|
||||||
|
DesktopErrorKind::Otp,
|
||||||
|
format!("entry does not contain an OTP URI: {entry}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
document.remove(field).map_err(DesktopError::document)?;
|
||||||
|
self.save_document(&document)?;
|
||||||
|
let document = EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, provider)
|
||||||
|
.map_err(DesktopError::document)?;
|
||||||
|
Ok(DesktopOtpMutation {
|
||||||
|
entry: entry.to_owned(),
|
||||||
|
document,
|
||||||
|
tree: self.tree()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mutate_active(
|
pub fn mutate_active(
|
||||||
&self,
|
&self,
|
||||||
handle: &NativeAuthenticationHandle,
|
handle: &NativeAuthenticationHandle,
|
||||||
@@ -611,6 +982,22 @@ impl DesktopStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn otp_field(document: &EntryDocument) -> Result<Option<EntryFieldId>, DesktopError> {
|
||||||
|
let mut fields = document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.filter(|field| field.metadata().kind() == EntryFieldKind::OtpUri)
|
||||||
|
.map(|field| field.id());
|
||||||
|
let first = fields.next();
|
||||||
|
if fields.next().is_some() {
|
||||||
|
return Err(DesktopError::new(
|
||||||
|
DesktopErrorKind::Otp,
|
||||||
|
format!("entry contains multiple OTP URIs: {}", document.path()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(first)
|
||||||
|
}
|
||||||
|
|
||||||
struct ConfigPolicyCommitter<'a> {
|
struct ConfigPolicyCommitter<'a> {
|
||||||
git: AutomaticPolicyCommitter,
|
git: AutomaticPolicyCommitter,
|
||||||
previous: &'a Config,
|
previous: &'a Config,
|
||||||
|
|||||||
@@ -550,20 +550,6 @@ fn classify_all(fields: &mut [EntryField]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
|
fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
|
||||||
if index == 0 {
|
|
||||||
return EntryFieldMetadata {
|
|
||||||
kind: EntryFieldKind::Password,
|
|
||||||
sensitivity: EntrySensitivity::Sensitive,
|
|
||||||
name: Some("password".to_owned()),
|
|
||||||
otp: None,
|
|
||||||
diagnostic: (!line.is_ascii() && std::str::from_utf8(line).is_err())
|
|
||||||
.then_some(EntryFieldDiagnostic::NonUtf8Value),
|
|
||||||
value: 0..line.len(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if line.is_empty() {
|
|
||||||
return blank_metadata();
|
|
||||||
}
|
|
||||||
if line.starts_with(b"otpauth://")
|
if line.starts_with(b"otpauth://")
|
||||||
&& let Ok(uri) = OtpUri::parse(SecretBytes::new(line.to_vec()))
|
&& let Ok(uri) = OtpUri::parse(SecretBytes::new(line.to_vec()))
|
||||||
{
|
{
|
||||||
@@ -585,6 +571,20 @@ fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
|
|||||||
value: 0..line.len(),
|
value: 0..line.len(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if index == 0 {
|
||||||
|
return EntryFieldMetadata {
|
||||||
|
kind: EntryFieldKind::Password,
|
||||||
|
sensitivity: EntrySensitivity::Sensitive,
|
||||||
|
name: Some("password".to_owned()),
|
||||||
|
otp: None,
|
||||||
|
diagnostic: (!line.is_ascii() && std::str::from_utf8(line).is_err())
|
||||||
|
.then_some(EntryFieldDiagnostic::NonUtf8Value),
|
||||||
|
value: 0..line.len(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if line.is_empty() {
|
||||||
|
return blank_metadata();
|
||||||
|
}
|
||||||
if line.starts_with(b"otpauth://") {
|
if line.starts_with(b"otpauth://") {
|
||||||
return EntryFieldMetadata {
|
return EntryFieldMetadata {
|
||||||
kind: EntryFieldKind::OtpUri,
|
kind: EntryFieldKind::OtpUri,
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ impl NativeClipboardManager {
|
|||||||
pub enum QrError {
|
pub enum QrError {
|
||||||
EmptyPayload,
|
EmptyPayload,
|
||||||
PayloadTooLarge,
|
PayloadTooLarge,
|
||||||
|
InvalidImage,
|
||||||
|
NotFound,
|
||||||
|
InvalidPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for QrError {
|
impl fmt::Display for QrError {
|
||||||
@@ -205,6 +208,9 @@ impl fmt::Display for QrError {
|
|||||||
match self {
|
match self {
|
||||||
Self::EmptyPayload => formatter.write_str("empty data cannot be encoded as a QR code"),
|
Self::EmptyPayload => formatter.write_str("empty data cannot be encoded as a QR code"),
|
||||||
Self::PayloadTooLarge => formatter.write_str("the QR payload is too large"),
|
Self::PayloadTooLarge => formatter.write_str("the QR payload is too large"),
|
||||||
|
Self::InvalidImage => formatter.write_str("the selected file is not a supported image"),
|
||||||
|
Self::NotFound => formatter.write_str("the image does not contain a QR code"),
|
||||||
|
Self::InvalidPayload => formatter.write_str("the QR code contains invalid text"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +239,26 @@ impl QrMatrix {
|
|||||||
Ok(Self { width, modules })
|
Ok(Self { width, modules })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode the first QR symbol from an encoded image without exposing the
|
||||||
|
/// secret-derived payload to a presentation adapter.
|
||||||
|
pub fn decode_image(image: &SecretBytes) -> Result<SecretBytes, QrError> {
|
||||||
|
let grayscale = image::load_from_memory(image.expose())
|
||||||
|
.map_err(|_| QrError::InvalidImage)?
|
||||||
|
.into_luma8();
|
||||||
|
let mut prepared = rqrr::PreparedImage::prepare_from_greyscale(
|
||||||
|
grayscale.width() as usize,
|
||||||
|
grayscale.height() as usize,
|
||||||
|
|x, y| grayscale.get_pixel(x as u32, y as u32).0[0],
|
||||||
|
);
|
||||||
|
let grid = prepared
|
||||||
|
.detect_grids()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(QrError::NotFound)?;
|
||||||
|
let (_, payload) = grid.decode().map_err(|_| QrError::InvalidPayload)?;
|
||||||
|
Ok(SecretBytes::new(payload.into_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn width(&self) -> usize {
|
pub fn width(&self) -> usize {
|
||||||
self.width
|
self.width
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,36 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pass_otp_only_entries_keep_typed_metadata_in_the_first_line() -> TestResult {
|
||||||
|
let fixture = FixtureSet::load()?;
|
||||||
|
let store = fixture.materialize_store("basic")?;
|
||||||
|
let repository = Repository::open(store.path())?;
|
||||||
|
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||||
|
let mut secrets = FixtureSecrets::all(&fixture);
|
||||||
|
write_plaintext(
|
||||||
|
&repository,
|
||||||
|
&keys,
|
||||||
|
"documents/otp-only",
|
||||||
|
b"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\n",
|
||||||
|
)?;
|
||||||
|
let document =
|
||||||
|
EntryDocumentService::new(&repository, &keys).open("documents/otp-only", &mut secrets)?;
|
||||||
|
assert_eq!(document.fields().len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
document.fields()[0].metadata().kind(),
|
||||||
|
EntryFieldKind::OtpUri
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
document.fields()[0]
|
||||||
|
.metadata()
|
||||||
|
.otp()
|
||||||
|
.and_then(|otp| otp.issuer()),
|
||||||
|
Some("Example")
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn field_ids_survive_updates_removal_and_reordering() -> TestResult {
|
fn field_ids_survive_updates_removal_and_reordering() -> TestResult {
|
||||||
let fixture = FixtureSet::load()?;
|
let fixture = FixtureSet::load()?;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use data_encoding::BASE32_NOPAD;
|
|||||||
use ironstorage::{
|
use ironstorage::{
|
||||||
command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest},
|
command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest},
|
||||||
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||||
|
desktop::{DesktopErrorKind, DesktopStorage},
|
||||||
git::{GitIdentity, GitRepository},
|
git::{GitIdentity, GitRepository},
|
||||||
otp::{OtpAlgorithm, OtpCodeValidity, OtpError, OtpInput, OtpKind, OtpService, OtpUri},
|
otp::{OtpAlgorithm, OtpCodeValidity, OtpError, OtpInput, OtpKind, OtpService, OtpUri},
|
||||||
recipient::RecipientPolicyManager,
|
recipient::RecipientPolicyManager,
|
||||||
@@ -513,6 +514,147 @@ fn automatic_code_supports_pass_diff_config_and_opens_git_only_for_hotp() -> Tes
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_otp_boundary_uses_rfc_codes_and_commits_import_replace_and_removal() -> TestResult {
|
||||||
|
let fixture = FixtureSet::load()?;
|
||||||
|
let store = fixture.materialize_store("basic")?;
|
||||||
|
let repository = Repository::open(store.path())?;
|
||||||
|
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||||
|
let recipients = RecipientPolicyManager::new(&repository, &keys)
|
||||||
|
.resolve_for_entry(&EntryPath::parse("desktop/rfc-totp")?, None)?;
|
||||||
|
let secret = BASE32_NOPAD.encode(b"12345678901234567890");
|
||||||
|
for (entry, plaintext) in [
|
||||||
|
(
|
||||||
|
"desktop/rfc-totp",
|
||||||
|
format!(
|
||||||
|
"password\notpauth://totp/RFC6238?secret={secret}&algorithm=SHA1&digits=8&period=30\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"desktop/rfc-hotp",
|
||||||
|
format!("password\notpauth://hotp/RFC4226?secret={secret}&counter=0\n"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
repository.write_entry(
|
||||||
|
&EntryPath::parse(entry)?,
|
||||||
|
&keys.encrypt(
|
||||||
|
SecretBytes::new(plaintext.into_bytes()),
|
||||||
|
recipients.recipients(),
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
GitRepository::init(&repository, GitIdentity::ironstorage())?;
|
||||||
|
let config_path = store.path().join("desktop-config.toml");
|
||||||
|
fs::write(
|
||||||
|
&config_path,
|
||||||
|
format!(
|
||||||
|
"vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
|
||||||
|
store.path(),
|
||||||
|
fixture.generated.keys[0].primary_fingerprint,
|
||||||
|
fixture.path("keys"),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
let desktop = DesktopStorage::load(Some(&config_path))?;
|
||||||
|
let mut provider = FixtureSecrets::all(&fixture);
|
||||||
|
|
||||||
|
let totp = desktop.otp_code("desktop/rfc-totp", 59, false, &mut provider)?;
|
||||||
|
assert_eq!(totp.code().expose(), b"94287082");
|
||||||
|
assert_eq!(
|
||||||
|
totp.validity(),
|
||||||
|
OtpCodeValidity::Timed {
|
||||||
|
valid_until: 60,
|
||||||
|
period: 30,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(totp.metadata().kind(), OtpKind::Totp);
|
||||||
|
assert_eq!(totp.metadata().digits(), 8);
|
||||||
|
|
||||||
|
let hotp_path = EntryPath::parse("desktop/rfc-hotp")?;
|
||||||
|
let before = repository.read_entry(&hotp_path)?;
|
||||||
|
let denied = match desktop.otp_code("desktop/rfc-hotp", 59, false, &mut provider) {
|
||||||
|
Ok(_) => panic!("HOTP requires confirmation"),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
assert_eq!(denied.kind(), DesktopErrorKind::Otp);
|
||||||
|
assert_eq!(repository.read_entry(&hotp_path)?, before);
|
||||||
|
let hotp = desktop.otp_code("desktop/rfc-hotp", 59, true, &mut provider)?;
|
||||||
|
assert_eq!(hotp.code().expose(), b"287082");
|
||||||
|
assert_eq!(
|
||||||
|
hotp.validity(),
|
||||||
|
OtpCodeValidity::CounterBased { counter: 1 }
|
||||||
|
);
|
||||||
|
let (_, _, _, tree, document) = hotp.into_parts();
|
||||||
|
assert!(tree.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
document
|
||||||
|
.expect("HOTP returns refreshed document")
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.find_map(|field| field.metadata().otp())
|
||||||
|
.and_then(|otp| otp.counter()),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = SecretBytes::new(
|
||||||
|
b"otpauth://totp/Replaced:alice?secret=JBSWY3DPEHPK3PXP&issuer=Replaced".to_vec(),
|
||||||
|
);
|
||||||
|
let before = repository.read_entry(&EntryPath::parse("desktop/rfc-totp")?)?;
|
||||||
|
assert!(
|
||||||
|
desktop
|
||||||
|
.import_otp(
|
||||||
|
"desktop/rfc-totp",
|
||||||
|
SecretBytes::new(replacement.expose().to_vec()),
|
||||||
|
OverwriteDecision::Decline,
|
||||||
|
&mut provider,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
repository.read_entry(&EntryPath::parse("desktop/rfc-totp")?)?,
|
||||||
|
before
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
desktop
|
||||||
|
.import_otp_qr(
|
||||||
|
"desktop/rfc-totp",
|
||||||
|
SecretBytes::new(b"not an image".to_vec()),
|
||||||
|
OverwriteDecision::Allow,
|
||||||
|
&mut provider,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
repository.read_entry(&EntryPath::parse("desktop/rfc-totp")?)?,
|
||||||
|
before
|
||||||
|
);
|
||||||
|
let replaced = desktop.import_otp(
|
||||||
|
"desktop/rfc-totp",
|
||||||
|
replacement,
|
||||||
|
OverwriteDecision::Allow,
|
||||||
|
&mut provider,
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
replaced
|
||||||
|
.into_parts()
|
||||||
|
.1
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.find_map(|field| field.metadata().otp())
|
||||||
|
.and_then(|otp| otp.issuer()),
|
||||||
|
Some("Replaced")
|
||||||
|
);
|
||||||
|
let removed = desktop.remove_otp("desktop/rfc-totp", &mut provider)?;
|
||||||
|
assert!(
|
||||||
|
removed
|
||||||
|
.into_parts()
|
||||||
|
.1
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.all(|field| field.metadata().kind() != ironstorage::document::EntryFieldKind::OtpUri)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_only_totp_ignores_git_that_mutations_correctly_reject() -> TestResult {
|
fn read_only_totp_ignores_git_that_mutations_correctly_reject() -> TestResult {
|
||||||
let fixture = FixtureSet::load()?;
|
let fixture = FixtureSet::load()?;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
|
io::Cursor,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -229,3 +230,39 @@ fn qr_matrices_round_trip_and_render_without_plaintext() -> TestResult {
|
|||||||
));
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn qr_image_import_returns_secret_bytes_and_rejects_invalid_input() -> TestResult {
|
||||||
|
let payload = SecretBytes::new(
|
||||||
|
b"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example".to_vec(),
|
||||||
|
);
|
||||||
|
let matrix = QrMatrix::encode(&payload)?;
|
||||||
|
let scale = 8_u32;
|
||||||
|
let quiet = 4_u32;
|
||||||
|
let size = (matrix.width() as u32 + quiet * 2) * scale;
|
||||||
|
let mut image = image::GrayImage::from_pixel(size, size, image::Luma([255]));
|
||||||
|
for y in 0..matrix.width() {
|
||||||
|
for x in 0..matrix.width() {
|
||||||
|
if matrix.is_dark(x, y) == Some(true) {
|
||||||
|
for dy in 0..scale {
|
||||||
|
for dx in 0..scale {
|
||||||
|
image.put_pixel(
|
||||||
|
(x as u32 + quiet) * scale + dx,
|
||||||
|
(y as u32 + quiet) * scale + dy,
|
||||||
|
image::Luma([0]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut encoded = Cursor::new(Vec::new());
|
||||||
|
image.write_to(&mut encoded, image::ImageFormat::Png)?;
|
||||||
|
let decoded = QrMatrix::decode_image(&SecretBytes::new(encoded.into_inner()))?;
|
||||||
|
assert_eq!(decoded.expose(), payload.expose());
|
||||||
|
assert!(matches!(
|
||||||
|
QrMatrix::decode_image(&SecretBytes::new(b"not an image".to_vec())),
|
||||||
|
Err(QrError::InvalidImage)
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user