From d9b0a6944a48c118d6ba694169f96caf302d2451 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Dec 2025 11:44:19 -0500 Subject: [PATCH] refactor: use secret agent for managing passwords --- Cargo.lock | 275 ++++--- Cargo.toml | 3 +- cosmic-settings/Cargo.toml | 2 + cosmic-settings/src/pages/networking/mod.rs | 3 + .../src/pages/networking/vpn/mod.rs | 371 ++++++--- .../src/pages/networking/vpn/nmcli.rs | 30 - cosmic-settings/src/pages/networking/wifi.rs | 192 +++-- subscriptions/network-manager/Cargo.toml | 7 +- subscriptions/network-manager/src/lib.rs | 365 +++++++-- .../network-manager/src/nm_secret_agent.rs | 740 ++++++++++++++++++ 10 files changed, 1611 insertions(+), 377 deletions(-) create mode 100644 subscriptions/network-manager/src/nm_secret_agent.rs diff --git a/Cargo.lock b/Cargo.lock index 68feef1..3170754 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -151,9 +162,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "aligned" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" dependencies = [ "as-slice", ] @@ -846,6 +857,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -884,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" dependencies = [ "byteorder", - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -899,15 +919,6 @@ dependencies = [ "zbus 5.12.0", ] -[[package]] -name = "branches" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11502672c5570f77f6bdf573332483f8475bab6a7fda00f1fae8ddb5a6245c0" -dependencies = [ - "rustc_version", -] - [[package]] name = "brotli-decompressor" version = "5.0.0" @@ -946,9 +957,9 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "by_address" @@ -1056,10 +1067,19 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.2.49" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cc" +version = "1.2.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ "find-msvc-tools", "jobserver", @@ -1095,9 +1115,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acd0bdbbf4b2612d09f52ba61da432140cb10930354079d0d53fafc12968726" +checksum = "21be0e1ce6cdb2ee7fff840f922fb04ead349e5cfb1e750b769132d44ce04720" dependencies = [ "smallvec", "target-lexicon", @@ -1144,6 +1164,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1463,7 +1493,7 @@ dependencies = [ [[package]] name = "cosmic-comp-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-comp#141fa472efb33427fdac2f817ef85763b2fbc5d0" +source = "git+https://github.com/pop-os/cosmic-comp#b1a4c3194a24a5c86fc5321442694466d4db3d86" dependencies = [ "cosmic-config", "input", @@ -1475,7 +1505,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1496,7 +1526,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "quote", "syn 2.0.111", @@ -1601,23 +1631,21 @@ dependencies = [ [[package]] name = "cosmic-randr" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-randr#f5923d1ef58b87ef103abe1a0e44460236d8fa36" +source = "git+https://github.com/pop-os/cosmic-randr#741089cf5e3aa7d5e48042101c1d4cc813b13637" dependencies = [ "cosmic-protocols", - "futures-lite 2.6.1", "indexmap 2.12.1", "thiserror 2.0.17", "tokio", "tracing", "wayland-client", "wayland-protocols-wlr", - "xutex", ] [[package]] name = "cosmic-randr-shell" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-randr#f5923d1ef58b87ef103abe1a0e44460236d8fa36" +source = "git+https://github.com/pop-os/cosmic-randr#741089cf5e3aa7d5e48042101c1d4cc813b13637" dependencies = [ "kdl", "slotmap", @@ -1677,6 +1705,7 @@ dependencies = [ "locale1", "locales-rs", "mime 0.3.17", + "nm-secret-agent-manager", "notify 6.1.1", "num-traits", "pwhash", @@ -1799,10 +1828,13 @@ dependencies = [ name = "cosmic-settings-network-manager-subscription" version = "1.0.0-beta6" dependencies = [ + "bitflags 2.10.0", "cosmic-dbus-networkmanager", "futures", "iced_futures", "itertools 0.14.0", + "nm-secret-agent-manager", + "secret-service", "secure-string", "thiserror 2.0.17", "tokio", @@ -1895,7 +1927,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "almost", "cosmic-config", @@ -1954,15 +1986,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -2202,6 +2225,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", + "subtle", ] [[package]] @@ -2783,9 +2807,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" +checksum = "824f08d01d0f496b3eca4f001a13cf17690a6ee930043d20817f547455fd98f8" dependencies = [ "autocfg", "tokio", @@ -3215,6 +3239,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.10.1" @@ -3225,6 +3258,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hostname-validator" version = "1.1.1" @@ -3333,7 +3375,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "dnd", "iced_accessibility", @@ -3351,7 +3393,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "accesskit", "accesskit_winit", @@ -3360,7 +3402,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "bitflags 2.10.0", "bytes", @@ -3385,7 +3427,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "futures", "iced_core", @@ -3411,7 +3453,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "bitflags 2.10.0", "bytemuck", @@ -3433,7 +3475,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3445,7 +3487,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -3461,7 +3503,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "bytemuck", "cosmic-text", @@ -3477,7 +3519,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "as-raw-xcb-connection", "bitflags 2.10.0", @@ -3508,7 +3550,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3529,7 +3571,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3999,7 +4041,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.0", - "zune-jpeg 0.5.6", + "zune-jpeg 0.5.7", ] [[package]] @@ -4102,6 +4144,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "input" version = "0.9.1" @@ -4549,7 +4601,7 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aabc8dcda530a6ac70617dd578cea55910af53c8" +source = "git+https://github.com/pop-os/libcosmic#fa26e0e2413cf5cd4c88201be8eb6ddd14917042" dependencies = [ "apply", "ashpd 0.12.0", @@ -4622,13 +4674,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall 0.6.0", ] [[package]] @@ -4987,9 +5039,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -5092,6 +5144,14 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nm-secret-agent-manager" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings//?branch=nm-secret-agent#f9153724b3d56f6c8051dc6697055a85c6d85d30" +dependencies = [ + "zbus 5.12.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -6058,7 +6118,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.9", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -6153,7 +6213,7 @@ checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" dependencies = [ "blowfish", "byteorder", - "hmac", + "hmac 0.10.1", "md-5", "rand 0.8.5", "sha-1", @@ -6290,9 +6350,9 @@ checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "rangemap" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "rav1e" @@ -6409,6 +6469,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -6595,15 +6664,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.37.28" @@ -6730,6 +6790,25 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "secret-service" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "getrandom 0.2.16", + "hkdf", + "num", + "once_cell", + "serde", + "sha2 0.10.9", + "zbus 5.12.0", +] + [[package]] name = "secure-string" version = "0.3.0" @@ -6746,12 +6825,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -6821,9 +6894,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -7298,7 +7371,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "version-compare", ] @@ -7565,14 +7638,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "indexmap 2.12.1", "serde_core", "serde_spanned", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -7586,9 +7659,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -7606,36 +7679,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow 0.7.14", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -7656,9 +7729,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -9161,16 +9234,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xutex" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f7b2794adabee656fae931dc27d457c4e8a83cfe96ad1e1c73de770b8401b57" -dependencies = [ - "branches", - "crossbeam-queue", -] - [[package]] name = "y4m" version = "0.8.0" @@ -9467,9 +9530,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f520eebad972262a1dde0ec455bce4f8b298b1e5154513de58c114c4c54303e8" +checksum = "51d915729b0e7d5fe35c2f294c5dc10b30207cc637920e5b59077bfa3da63f28" dependencies = [ "zune-core 0.5.0", ] diff --git a/Cargo.toml b/Cargo.toml index 1d0d1a8..7acd839 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,9 +70,10 @@ cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", # cosmic-theme = { path = "../libcosmic/cosmic-theme" } # iced_futures = { path = "../libcosmic/iced/futures" } -# [patch.'https://github.com/pop-os/dbus-settings-bindings'] +[patch.'https://github.com/pop-os/dbus-settings-bindings'] # cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" } # upower_dbus = { path = "../dbus-settings-bindings/upower" } +nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings//", branch = "nm-secret-agent" } [patch."https://github.com/smithay/client-toolkit.git"] sctk = { package = "smithay-client-toolkit", version = "0.20.0" } diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 1b0a056..98fbb3a 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -18,6 +18,7 @@ cosmic-bg-config.workspace = true cosmic-comp-config = { workspace = true, optional = true } cosmic-config.workspace = true cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } +nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } cosmic-idle-config.workspace = true cosmic-panel-config = { workspace = true, optional = true } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true } @@ -164,6 +165,7 @@ page-networking = [ "dep:cosmic-settings-network-manager-subscription", "xdg-portal", "dep:cosmic-dbus-networkmanager", + "dep:nm-secret-agent-manager", "dep:zbus", ] page-power = ["dep:upower_dbus", "dep:zbus"] diff --git a/cosmic-settings/src/pages/networking/mod.rs b/cosmic-settings/src/pages/networking/mod.rs index 5f261fa..1692547 100644 --- a/cosmic-settings/src/pages/networking/mod.rs +++ b/cosmic-settings/src/pages/networking/mod.rs @@ -16,8 +16,11 @@ use cosmic_dbus_networkmanager::{ use cosmic_settings_network_manager_subscription as network_manager; use cosmic_settings_page::{self as page, Section, section}; use futures::StreamExt; +use secure_string::SecureString; use slotmap::SlotMap; +pub type SecretSender = Arc>>>; + static NM_CONNECTION_EDITOR: &str = "nm-connection-editor"; #[derive(Debug, Default)] diff --git a/cosmic-settings/src/pages/networking/vpn/mod.rs b/cosmic-settings/src/pages/networking/vpn/mod.rs index 16f28b8..589528b 100644 --- a/cosmic-settings/src/pages/networking/vpn/mod.rs +++ b/cosmic-settings/src/pages/networking/vpn/mod.rs @@ -1,8 +1,9 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only -mod nmcli; +pub mod nmcli; +use std::collections::HashMap; use std::sync::Arc; use anyhow::Context; @@ -13,6 +14,7 @@ use cosmic::{ iced_core::text::Wrapping, widget::{self, icon}, }; +use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag}; use cosmic_settings_network_manager_subscription::{ self as network_manager, NetworkManagerState, UUID, current_networks::ActiveConnectionInfo, }; @@ -20,6 +22,9 @@ use cosmic_settings_page::{self as page, Section, section}; use futures::{FutureExt, StreamExt}; use indexmap::IndexMap; use secure_string::SecureString; +use tokio::sync::Mutex; + +use crate::pages::networking::SecretSender; pub type ConnectionId = Arc; pub type InterfaceId = String; @@ -36,6 +41,8 @@ pub enum Message { CancelDialog, /// Connect to a VPN with the given username and password ConnectWithPassword, + /// Connect to a VPN with the given username and password + RetryWithPassword, /// Deactivate a connection. Deactivate(ConnectionId), /// An error occurred. @@ -46,6 +53,8 @@ pub enum Message { KnownConnections(IndexMap), /// An update from the network manager daemon NetworkManager(network_manager::Event), + /// An update from the secret agent + SecretAgent(network_manager::nm_secret_agent::Event), /// Successfully connected to the system dbus. NetworkManagerConnect(zbus::Connection), /// Updates the password text input @@ -146,31 +155,17 @@ enum ConnectionType { Password, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum PasswordFlag { - /// The system is responsible for providing and storing this secret. - None = 0, - /// A user-session secret agent is responsible for providing and storing - /// this secret; when it is required, agents will be asked to provide it. - AgentOwned = 1, - /// This secret should not be saved but should be requested from the user - /// each time it is required. This flag should be used for One-Time-Pad - /// secrets, PIN codes from hardware tokens, or if the user simply does not - /// want to save the secret. - NotSaved = 2, - /// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user. - NotRequired = 4, -} - -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug)] pub enum VpnDialog { Error(ErrorKind, String), Password { id: String, uuid: Arc, - username: String, + username: Option, password: SecureString, + description: Option, password_hidden: bool, + tx: SecretSender, error: Option<(ErrorKind, String)>, }, RemoveProfile(ConnectionId), @@ -189,6 +184,7 @@ pub struct NmState { pub struct Page { entity: page::Entity, nm_task: Option>, + secret_tx: Option>, nm_state: Option, dialog: Option, view_more_popup: Option, @@ -242,10 +238,13 @@ impl page::Page for Page { password, password_hidden, error, + description, .. } => { - let username = widget::text_input(fl!("username"), username.as_str()) - .on_input(Message::UsernameUpdate); + let username = username.as_ref().map(|username| { + widget::text_input(fl!("username"), username.as_str()) + .on_input(Message::UsernameUpdate) + }); let password = widget::text_input::secure_input( fl!("password"), @@ -254,8 +253,14 @@ impl page::Page for Page { *password_hidden, ) .on_input(|input| Message::PasswordUpdate(SecureString::from(input))) - .on_submit(|_| Message::ConnectWithPassword); - let (err_kind, error) = if let Some(err) = error.as_ref() { + .on_submit(|_| { + if error.is_some() { + Message::RetryWithPassword + } else { + Message::ConnectWithPassword + } + }); + let (err_kind, error_text) = if let Some(err) = error.as_ref() { ( Some(err.0), Some(widget::text::body(err.1.as_str()).wrapping(Wrapping::Word)), @@ -266,13 +271,17 @@ impl page::Page for Page { let controls = widget::column::with_capacity(2) .spacing(12) - .push(username) + .push_maybe(username) .push(password) - .push_maybe(error) + .push_maybe(error_text) .apply(Element::from); - let primary_action = widget::button::suggested(fl!("connect")) - .on_press(Message::ConnectWithPassword); + let primary_action = + widget::button::suggested(fl!("connect")).on_press(if error.is_some() { + Message::RetryWithPassword + } else { + Message::ConnectWithPassword + }); let secondary_action = widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog); @@ -284,7 +293,11 @@ impl page::Page for Page { fl!("auth-dialog") }) .icon(icon::from_name("network-vpn-symbolic").size(64)) - .body(fl!("auth-dialog", "vpn-description")) + .body(if let Some(description) = description.as_ref() { + description.clone() + } else { + fl!("auth-dialog", "vpn-description") + }) .control(controls) .primary_action(primary_action) .secondary_action(secondary_action) @@ -347,8 +360,10 @@ impl page::Page for Page { } fn on_enter(&mut self) -> cosmic::Task { + let (tx, rx) = tokio::sync::mpsc::channel(4); + self.secret_tx = Some(tx); if self.nm_task.is_none() { - return cosmic::task::future(async move { + return cosmic::Task::batch([cosmic::task::future(async move { zbus::Connection::system() .await .context("failed to create system dbus connection") @@ -356,10 +371,15 @@ impl page::Page for Page { |why| Message::Error(ErrorKind::DbusConnection, why.to_string()), Message::NetworkManagerConnect, ) - }); + }), + cosmic::Task::stream( + cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", rx), + ) + .map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))), + ]); } - Task::none() + cosmic::Task::none() } fn on_leave(&mut self) -> Task { @@ -401,19 +421,15 @@ impl Page { ]); } } - Message::KnownConnections(connections) => { self.known_connections = connections; } - Message::UpdateDevices(devices) => { self.update_devices(devices); } - Message::UpdateState(state) => { self.update_active_conns(state); } - Message::NetworkManager( network_manager::Event::ActiveConns | network_manager::Event::Devices, ) => { @@ -425,7 +441,6 @@ impl Page { ]); } } - Message::NetworkManager(network_manager::Event::Init { conn, sender, @@ -447,21 +462,16 @@ impl Page { update_devices(conn), ]); } - Message::NetworkManager(_event) => (), - Message::AddNetwork => return add_network(), - Message::AddWireGuardDevice(device, filename, path) => { self.dialog = Some(VpnDialog::WireGuardName(device, filename, path)); } - Message::WireGuardDeviceInput(input) => { if let Some(VpnDialog::WireGuardName(ref mut device, ..)) = self.dialog { *device = input } } - Message::WireGuardConfig => { if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() { return cosmic::task::future(async move { @@ -474,7 +484,6 @@ impl Page { }); } } - Message::Activate(uuid) => { self.close_popup_and_apply_updates(); @@ -502,13 +511,14 @@ impl Page { self.dialog = Some(VpnDialog::Password { id: settings.id.clone(), uuid: uuid.clone(), - username: settings.username.clone().unwrap_or_default(), + username: settings.username.clone(), password: SecureString::from(""), + description: None, password_hidden: true, error: None, + tx: Arc::new(Mutex::new(None)), }); } - _ => { let connection_name = settings.id.clone(); let username = settings.username.clone(); @@ -521,9 +531,12 @@ impl Page { )), id: connection_name.clone(), uuid, - username: username.clone().unwrap_or_default(), + username: username.clone(), + description: None, password: SecureString::from(""), password_hidden: true, + // TODO grab from the current dialog + tx: Arc::new(Mutex::new(None)), }); } @@ -533,19 +546,16 @@ impl Page { } } } - Message::Deactivate(uuid) => { self.close_popup_and_apply_updates(); if let Some(NmState { ref sender, .. }) = self.nm_state { _ = sender.unbounded_send(network_manager::Request::Deactivate(uuid)); } } - Message::RemoveProfileRequest(uuid) => { self.view_more_popup = None; self.dialog = Some(VpnDialog::RemoveProfile(uuid)); } - Message::RemoveProfile(uuid) => { self.dialog = None; self.close_popup_and_apply_updates(); @@ -553,7 +563,6 @@ impl Page { _ = sender.unbounded_send(network_manager::Request::Remove(uuid)); } } - Message::ViewMore(uuid) => { self.view_more_popup = uuid; if self.view_more_popup.is_none() { @@ -576,7 +585,6 @@ impl Page { .await }); } - Message::Refresh => { if let Some(NmState { ref conn, .. }) = self.nm_state { return cosmic::Task::batch(vec![ @@ -586,7 +594,6 @@ impl Page { ]); } } - Message::PasswordUpdate(pass) => { if let Some(VpnDialog::Password { ref mut password, .. @@ -595,12 +602,105 @@ impl Page { *password = pass; } } - Message::ConnectWithPassword => { let Some(dialog) = self.dialog.take() else { return Task::none(); }; + if let VpnDialog::Password { + id, + uuid, + username, + password, + tx, + .. + } = dialog + { + let username_unwrapped = username.clone().unwrap_or_default(); + let task = self.activate_with_password( + id.clone(), + uuid.clone(), + username_unwrapped.clone(), + password.clone(), + ); + let sec_tx = self.secret_tx.clone(); + return task + .then(move |_| { + let sec_tx = sec_tx.clone(); + let uuid = uuid.clone(); + let username = username.clone(); + let password = password.clone(); + let tx = tx.clone(); + let id = id.clone(); + Task::future(async move { + let mut guard = tx.lock().await; + if let Some(sender) = guard.take() { + let _ = sender.send(password); + } else { + // apply password and username then + if let Some(sec_tx) = sec_tx { + let (applied_tx, applied_rx) = + tokio::sync::oneshot::channel(); + if let Err(err) = sec_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: "vpn".to_string(), + uuid: uuid.to_string(), + secrets: HashMap::from_iter([ + // username and password + ( + "username".to_string(), + username.clone().unwrap_or_default().into(), + ), + ( + "password".to_string(), + password.clone().into(), + ), + ]), + applied_tx, + }) + .await + { + tracing::error!(%err, "failed to apply secret"); + } + // wait max 1s for the applied signal + if let Err(err) = tokio::time::timeout( + std::time::Duration::from_secs(1), + applied_rx, + ) + .await + { + tracing::error!(%err, "failed to apply secret"); + } + } + // activate + if let Err(why) = nmcli::connect(&id).await { + return Message::VpnDialogError(VpnDialog::Password { + error: Some(( + ErrorKind::Connect, + format!("failed to connect to VPN: {why}"), + )), + id: id.clone(), + uuid, + username: username.clone(), + description: None, + password, + password_hidden: true, + tx: Arc::new(Mutex::new(None)), + }); + } + } + + Message::Refresh + }) + }) + .map(crate::app::Message::from); + } + } + Message::RetryWithPassword => { + let Some(dialog) = self.dialog.take() else { + return Task::none(); + }; + if let VpnDialog::Password { id, uuid, @@ -609,18 +709,54 @@ impl Page { .. } = dialog { - return self - .activate_with_password(id, uuid, username, password) + let username_unwrapped = username.unwrap_or_default(); + let sec_tx = self.secret_tx.clone(); + let task = self.activate_with_password( + id.clone(), + uuid.clone(), + username_unwrapped.clone(), + password.clone(), + ); + return task + .then(move |_| { + let sec_tx = sec_tx.clone(); + let uuid = uuid.clone(); + let username = username_unwrapped.clone(); + let password = password.clone(); + Task::future(async move { + if let Some(sec_tx) = sec_tx { + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + let _ = sec_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: "vpn".to_string(), + uuid: uuid.to_string(), + secrets: HashMap::from_iter([ + // username and password + ("username".to_string(), username.clone().into()), + ("password".to_string(), password.clone().into()), + ]), + applied_tx, + }) + .await; + // wait max 1s for the applied signal + let _ = tokio::time::timeout( + std::time::Duration::from_secs(1), + applied_rx, + ) + .await; + } + Message::Activate(uuid) + }) + }) .map(crate::app::Message::from); } } - Message::UsernameUpdate(user) => { if let Some(VpnDialog::Password { ref mut username, .. }) = self.dialog { - *username = user; + *username = Some(user); } } Message::CancelDialog => { @@ -635,19 +771,65 @@ impl Page { *password_hidden = !*password_hidden; } } - Message::Error(error_kind, why) => { tracing::error!(?error_kind, why); self.dialog = Some(VpnDialog::Error(error_kind, why)) } - Message::NetworkManagerConnect(conn) => { return self.connect(conn.clone()); } - Message::VpnDialogError(vpn_dialog) => { self.dialog = Some(vpn_dialog); } + Message::SecretAgent(e) => match e { + nm_secret_agent::Event::RequestSecret { + uuid: _, + name, + previous, + description, + tx, + } => { + self.dialog = Some(VpnDialog::Password { + id: name.clone(), + uuid: Arc::from(""), + username: None, + description: description, + password: previous, + password_hidden: true, + tx, + error: None, + }); + } + nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => { + self.dialog = self + .dialog + .take() + .filter(|d| !matches!(d, &VpnDialog::Password { .. })); + } + nm_secret_agent::Event::Failed(error) => { + tracing::error!(%error, "secret agent failure"); + if let Some(VpnDialog::Password { + id, + uuid, + username, + password, + description, + .. + }) = self.dialog.take() + { + self.dialog = Some(VpnDialog::Password { + error: Some((ErrorKind::DbusConnection, error.to_string())), + id, + uuid, + username, + description, + password, + password_hidden: true, + tx: Arc::new(Mutex::new(None)), + }); + } + } + }, } Task::none() @@ -666,20 +848,11 @@ impl Page { error: Some((ErrorKind::WithPassword("username"), why.to_string())), id: connection_name.clone(), uuid, - username, - password, - password_hidden: true, - }); - } - - if let Err(why) = nmcli::set_password_flags_none(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::WithPassword("password-flags"), why.to_string())), - id: connection_name.clone(), - uuid, - username, + username: Some(username), + description: None, password, password_hidden: true, + tx: Arc::new(Mutex::new(None)), }); } @@ -688,42 +861,11 @@ impl Page { error: Some((ErrorKind::Config, why.to_string())), id: connection_name.clone(), uuid, - username, - password, - password_hidden: true, - }); - } - - if let Err(why) = nmcli::set_password_flags_none(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::WithPassword("password-flags"), why.to_string())), - id: connection_name.clone(), - uuid, - username, - password, - password_hidden: true, - }); - } - - if let Err(why) = nmcli::set_password(&connection_name, password.unsecure()).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::WithPassword("password"), why.to_string())), - id: connection_name.clone(), - uuid, - username, - password, - password_hidden: true, - }); - } - - if let Err(why) = nmcli::connect(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::Connect, why.to_string())), - id: connection_name.clone(), - uuid, - username, + username: Some(username), password, + description: None, password_hidden: true, + tx: Arc::new(Mutex::new(None)), }); } @@ -1065,25 +1207,24 @@ fn connection_settings(conn: zbus::Connection) -> Task { let id = connection.get("id")?.downcast_ref::().ok()?; let uuid = connection.get("uuid")?.downcast_ref::().ok()?; - let (username, connection_type, password_flag) = vpn + let (connection_type, username, password_flag) = vpn .get("data") .and_then(|data| data.downcast_ref::().ok()) .map(|dict| { let (mut connection_type, mut password_flag) = (None, None); - - let username = dict - .get::(&String::from("username")) - .ok() - .flatten() - .filter(|value| !value.is_empty()); - - if let Some("password") = dict + let mut username = vpn + .get("user-name") + .and_then(|u| u.downcast_ref::().ok()); + if dict .get::(&String::from("connection-type")) .ok() .flatten() .as_deref() + // may be "password" or "password-tls" + .is_some_and(|p| p.starts_with("password")) { connection_type = Some(ConnectionType::Password); + username = Some(username.unwrap_or_default()); password_flag = dict .get::(&String::from("password-flags")) @@ -1098,7 +1239,7 @@ fn connection_settings(conn: zbus::Connection) -> Task { }); } - (username, connection_type, password_flag) + (connection_type, username, password_flag) }) .unwrap_or_default(); diff --git a/cosmic-settings/src/pages/networking/vpn/nmcli.rs b/cosmic-settings/src/pages/networking/vpn/nmcli.rs index 878bccd..c759c14 100644 --- a/cosmic-settings/src/pages/networking/vpn/nmcli.rs +++ b/cosmic-settings/src/pages/networking/vpn/nmcli.rs @@ -13,21 +13,6 @@ pub async fn set_username(connection_name: &str, username: &str) -> Result<(), S .apply(crate::utils::map_stderr_output) } -pub async fn set_password_flags_none(connection_name: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args([ - "con", - "mod", - connection_name, - "+vpn.data", - "password-flags=0", - ]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - pub async fn add_fallback(connection_name: &str) -> Result<(), String> { tokio::process::Command::new("nmcli") .args([ @@ -43,21 +28,6 @@ pub async fn add_fallback(connection_name: &str) -> Result<(), String> { .apply(crate::utils::map_stderr_output) } -pub async fn set_password(connection_name: &str, password: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args([ - "con", - "mod", - connection_name, - "vpn.secrets", - &format!("password={password}"), - ]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - pub async fn connect(connection_name: &str) -> Result<(), String> { tokio::process::Command::new("nmcli") .args(["con", "up", connection_name]) diff --git a/cosmic-settings/src/pages/networking/wifi.rs b/cosmic-settings/src/pages/networking/wifi.rs index 02cf506..9e6f89f 100644 --- a/cosmic-settings/src/pages/networking/wifi.rs +++ b/cosmic-settings/src/pages/networking/wifi.rs @@ -20,10 +20,14 @@ use cosmic_settings_network_manager_subscription::{ available_wifi::{AccessPoint, NetworkType}, current_networks::ActiveConnectionInfo, hw_address::HwAddress, + nm_secret_agent, }; use cosmic_settings_page::{self as page, Section, section}; use futures::StreamExt; use secure_string::SecureString; +use tokio::sync::Mutex; + +use crate::pages::networking::SecretSender; #[derive(Clone, Debug)] pub enum Message { @@ -49,6 +53,8 @@ pub enum Message { Forget(network_manager::SSID), /// An update from the network manager daemon NetworkManager(network_manager::Event), + /// An update from the secret agent + SecretAgent(network_manager::nm_secret_agent::Event), /// Successfully connected to the system dbus. NetworkManagerConnect(zbus::Connection), /// Request an auth dialog @@ -87,7 +93,7 @@ impl From for crate::pages::Message { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] enum WiFiDialog { Forget(network_manager::SSID), Password { @@ -96,6 +102,7 @@ enum WiFiDialog { identity: Option, password: SecureString, password_hidden: bool, + tx: SecretSender, }, } @@ -103,7 +110,7 @@ enum WiFiDialog { #[derive(Clone, Debug, PartialEq, Eq)] struct QRCodeDrawer { ssid: network_manager::SSID, - password: Option, + password: Option, security_type: NetworkType, } @@ -111,6 +118,7 @@ struct QRCodeDrawer { pub struct Page { entity: page::Entity, nm_task: Option>, + secret_tx: Option>, nm_state: Option, /// When defined, displays connections for the specific device. active_device: Option>, @@ -245,7 +253,7 @@ impl page::Page for Page { )); if let Some(ref pass) = drawer.password { - info_items = info_items.add(widget::settings::item(fl!("password"), pass.as_str())); + info_items = info_items.add(widget::settings::item(fl!("password"), pass.unsecure())); } let content = column::column() @@ -279,8 +287,10 @@ impl page::Page for Page { } fn on_enter(&mut self) -> cosmic::Task { + let (tx, rx) = tokio::sync::mpsc::channel(4); + self.secret_tx = Some(tx); if self.nm_task.is_none() { - return cosmic::Task::future(async move { + return Task::batch(vec![cosmic::Task::future(async move { zbus::Connection::system() .await .context("failed to create system dbus connection") @@ -289,7 +299,10 @@ impl page::Page for Page { Message::NetworkManagerConnect, ) .apply(crate::pages::Message::WiFi) - }); + }), cosmic::Task::stream( + cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", rx), + ) + .map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m)))]); } Task::none() @@ -350,16 +363,30 @@ impl Page { hw_address, password: SecureString::from(""), password_hidden: true, + tx: Arc::new(Mutex::new(None)), }); } } network_manager::Request::SelectAccessPoint( ssid, - _hw_address, - _network_type, + hw_address, + network_type, + _tx, ) => { - self.connecting.remove(ssid.as_ref()); + if success || matches!(network_type, NetworkType::Open) { + self.connecting.remove(ssid.as_ref()); + } else { + self.dialog = Some(WiFiDialog::Password { + ssid: ssid.into(), + identity: matches!(network_type, NetworkType::EAP) + .then(String::new), + hw_address, + password: SecureString::from(""), + password_hidden: true, + tx: Arc::new(Mutex::new(None)), + }); + } } _ => (), @@ -371,11 +398,9 @@ impl Page { return update_devices(conn.clone()); } } - Message::UpdateDevices(devices) => { self.update_devices(devices); } - Message::UpdateState(state) => { self.update_state(state); @@ -383,7 +408,6 @@ impl Page { return connection_settings(conn.clone()); } } - Message::NetworkManager( network_manager::Event::ActiveConns | network_manager::Event::Devices @@ -397,11 +421,9 @@ impl Page { ]); } } - Message::ConnectionSettings(settings) => { self.ssid_to_uuid = settings; } - Message::NetworkManager(network_manager::Event::Init { conn, sender, @@ -416,7 +438,6 @@ impl Page { return update_devices(conn); } - Message::NetworkManager(network_manager::Event::WiFiCredentials { ssid, password, @@ -431,7 +452,7 @@ impl Page { NetworkType::EAP => "WPA", NetworkType::Open => "", }; - let escaped_password = escape_wifi_qr_string(pass); + let escaped_password = escape_wifi_qr_string(pass.unsecure()); format!( "WIFI:T:{};S:{};P:{};;", security, escaped_ssid, escaped_password @@ -451,11 +472,9 @@ impl Page { // Open the context drawer return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity)); } - Message::AddNetwork => { tokio::task::spawn(super::nm_add_wifi()); } - Message::Connect(ssid) => { if let Some(nm) = self.nm_state.as_mut() { let Some(ap) = nm @@ -474,10 +493,10 @@ impl Page { ssid, ap.hw_address, ap.network_type, + self.secret_tx.clone(), )); } } - Message::IdentityUpdate(new_identity) => { if let Some(WiFiDialog::Password { ref mut identity, .. @@ -486,7 +505,6 @@ impl Page { *identity = Some(new_identity); } } - Message::PasswordRequest(ssid) => { if let Some(nm) = self.nm_state.as_mut() { let Some(ap) = nm @@ -498,16 +516,17 @@ impl Page { else { return Task::none(); }; + self.dialog = Some(WiFiDialog::Password { ssid, identity: matches!(ap.network_type, NetworkType::EAP).then(String::new), hw_address: ap.hw_address, password: SecureString::from(""), password_hidden: true, + tx: Arc::new(Mutex::new(None)), }); } } - Message::PasswordUpdate(pass) => { if let Some(WiFiDialog::Password { ref mut password, .. @@ -516,7 +535,6 @@ impl Page { *password = pass; } } - Message::ConnectWithPassword => { let Some(dialog) = self.dialog.take() else { return Task::none(); @@ -527,22 +545,31 @@ impl Page { identity, password, hw_address, + tx, .. } = dialog && let Some(nm) = self.nm_state.as_mut() { self.connecting.insert(ssid.clone()); - _ = nm - .sender - .unbounded_send(network_manager::Request::Authenticate { - ssid: ssid.to_string(), - identity, - hw_address, - password, - }); + let nm_sender = nm.sender.clone(); + let secret_tx = self.secret_tx.clone(); + return Task::future(async move { + let mut guard = tx.lock().await; + if let Some(tx) = guard.take() { + _ = tx.send(password); + } else { + _ = nm_sender.unbounded_send(network_manager::Request::Authenticate { + ssid: ssid.to_string(), + identity, + hw_address, + password, + secret_tx, + }); + } + }) + .discard(); } } - Message::TogglePasswordVisibility => { if let Some(WiFiDialog::Password { ref mut password_hidden, @@ -552,23 +579,39 @@ impl Page { *password_hidden = !*password_hidden; } } - Message::QRCodeRequest(ssid) => { self.view_more_popup = None; - if let Some(nm) = self.nm_state.as_mut() { + let Some(ap): Option<&AccessPoint> = self.nm_state.as_ref().and_then(|nm| { + nm.state + .wireless_access_points + .iter() + .chain(nm.state.known_access_points.iter()) + .find(|ap| ap.ssid == ssid) + }) else { + return Task::none(); + }; + if let Some(nm) = self.nm_state.as_ref() { + let uuid = self + .ssid_to_uuid + .get(ssid.as_ref()) + .map(|uuid| uuid.as_ref()) + .unwrap_or_default(); _ = nm .sender - .unbounded_send(network_manager::Request::GetWiFiCredentials(ssid)); + .unbounded_send(network_manager::Request::GetWiFiCredentials( + ssid, + uuid.into(), + ap.network_type, + self.secret_tx.clone(), + )); } } - Message::ViewMore(ssid) => { self.view_more_popup = ssid; if self.view_more_popup.is_none() { self.close_popup_and_apply_updates(); } } - Message::Disconnect(ssid) => { self.close_popup_and_apply_updates(); if let Some(nm) = self.nm_state.as_mut() { @@ -577,12 +620,10 @@ impl Page { .unbounded_send(network_manager::Request::Disconnect(ssid)); } } - Message::ForgetRequest(ssid) => { self.dialog = Some(WiFiDialog::Forget(ssid)); self.view_more_popup = None; } - Message::Forget(ssid) => { self.dialog = None; self.close_popup_and_apply_updates(); @@ -592,7 +633,6 @@ impl Page { .unbounded_send(network_manager::Request::Forget(ssid)); } } - Message::Settings(ssid) => { self.close_popup_and_apply_updates(); @@ -602,13 +642,11 @@ impl Page { ); } } - Message::SubmitIdentity => { if self.dialog.is_some() { return focus_next(); } } - Message::WiFiEnable(enable) => { if let Some(nm) = self.nm_state.as_mut() { _ = nm @@ -617,26 +655,92 @@ impl Page { _ = nm.sender.unbounded_send(network_manager::Request::Reload); } } - Message::CancelDialog => { self.dialog = None; } - Message::Error(why) => { tracing::error!(why); } - Message::SelectDevice(device) => { // TODO: Per-device wifi connection handling. self.active_device = Some(device); } - Message::NetworkManagerConnect(conn) => { return cosmic::task::batch(vec![ self.connect(conn.clone()), connection_settings(conn), ]); } + Message::SecretAgent(event) => match event { + nm_secret_agent::Event::RequestSecret { + uuid, + name, + description: _, // TODO do we want to display the description? + previous, + tx, + } => { + let ssid = self + .ssid_to_uuid + .iter() + .find_map(|(ssid, conn_uuid)| { + if conn_uuid.as_ref() == name.as_str() { + Some(network_manager::SSID::from(ssid.as_ref())) + } else { + None + } + }) + .unwrap_or_default(); + let Some(ap): Option<&AccessPoint> = self.nm_state.as_ref().and_then(|nm| { + nm.state + .wireless_access_points + .iter() + .chain(nm.state.known_access_points.iter()) + .find(|ap| ap.ssid == ssid) + }) else { + tracing::error!( + %uuid, + %name, + "received secret request for unknown connection" + ); + return Task::none(); + }; + + self.dialog = Some(WiFiDialog::Password { + ssid, + password: previous, + password_hidden: true, + hw_address: ap.hw_address, + identity: matches!(ap.network_type, NetworkType::EAP).then(String::new), + tx, + }); + } + nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => { + self.dialog = self + .dialog + .take() + .filter(|d| !matches!(d, &WiFiDialog::Password { .. })); + } + nm_secret_agent::Event::Failed(error) => { + tracing::error!(%error, "secret agent failure"); + if let Some(WiFiDialog::Password { + ssid, + password, + identity, + hw_address, + .. + }) = self.dialog.take() + { + self.dialog = Some(WiFiDialog::Password { + password, + password_hidden: true, + tx: Arc::new(Mutex::new(None)), + ssid, + identity, + hw_address, + }); + } + } + }, } Task::none() diff --git a/subscriptions/network-manager/Cargo.toml b/subscriptions/network-manager/Cargo.toml index c7ef348..7500052 100644 --- a/subscriptions/network-manager/Cargo.toml +++ b/subscriptions/network-manager/Cargo.toml @@ -7,7 +7,9 @@ rust-version.workspace = true publish = true [dependencies] -cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" } +cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" } +secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"] } +nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings" } futures = "0.3.31" iced_futures = { git = "https://github.com/pop-os/libcosmic" } itertools = "0.14.0" @@ -15,4 +17,5 @@ secure-string = "0.3.0" thiserror = "2.0.17" tokio = "1.48.0" tracing = "0.1.41" -zbus = "5.12.0" +zbus = { version = "5.12.0", features = ["tokio"] } +bitflags = "2.10.0" diff --git a/subscriptions/network-manager/src/lib.rs b/subscriptions/network-manager/src/lib.rs index 65378dc..d36b766 100644 --- a/subscriptions/network-manager/src/lib.rs +++ b/subscriptions/network-manager/src/lib.rs @@ -6,6 +6,7 @@ pub mod available_wifi; pub mod current_networks; pub mod devices; pub mod hw_address; +pub mod nm_secret_agent; pub mod wireless_enabled; use std::{collections::HashMap, fmt::Debug, sync::Arc, time::Duration}; @@ -18,11 +19,11 @@ use cosmic_dbus_networkmanager::{ active_connection::ActiveConnection, device::SpecificDevice, interface::{ - active_connection::ActiveConnectionProxy, enums::{self, ActiveConnectionState, DeviceType, NmConnectivityState}, + settings::connection::ConnectionSettingsProxy, }, nm::NetworkManager, - settings::NetworkManagerSettings, + settings::{NetworkManagerSettings, connection::Connection}, }; use futures::{ FutureExt, SinkExt, StreamExt, @@ -54,6 +55,8 @@ pub enum Error { NoWiFiDevices, #[error("zbus error")] Zbus(#[from] zbus::Error), + #[error("missing connection field")] + MissingField(&'static str), } #[derive(Debug)] @@ -297,8 +300,10 @@ async fn start_listening( identity, password, hw_address, + secret_tx, }) => { let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); + let success = nm_state .connect_wifi( &conn, @@ -306,6 +311,12 @@ async fn start_listening( identity.as_deref(), Some(password.unsecure()), hw_address, + secret_tx.clone(), + if identity.is_some() { + NetworkType::EAP + } else { + NetworkType::PSK + }, ) .await .is_ok(); @@ -317,6 +328,7 @@ async fn start_listening( identity: identity.clone(), password: password.clone(), hw_address, + secret_tx: secret_tx.clone(), }, success, state: NetworkManagerState::new(&conn).await.unwrap_or_default(), @@ -324,19 +336,35 @@ async fn start_listening( .await; } - Some(Request::SelectAccessPoint(ssid, hw_address, network_type)) => { + Some(Request::SelectAccessPoint(ssid, hw_address, network_type, secret_tx)) => { if matches!(network_type, NetworkType::Open) { - attempt_wifi_connection(&conn, ssid, hw_address, network_type, output) - .await; + attempt_wifi_connection( + &conn, + ssid, + hw_address, + network_type, + output, + None, + ) + .await; } else { // For secured networks, check if we have saved credentials - if !has_saved_wifi_credentials(&conn, &ssid).await { + let has_saved = has_saved_wifi_credentials(&conn, &ssid).await; + + if !has_saved { return State::Waiting(conn, rx); } // We have saved credentials, attempt connection - attempt_wifi_connection(&conn, ssid, hw_address, network_type, output) - .await; + attempt_wifi_connection( + &conn, + ssid, + hw_address, + network_type, + output, + secret_tx, + ) + .await; } } @@ -451,7 +479,7 @@ async fn start_listening( .await; } - Some(Request::GetWiFiCredentials(ssid)) => { + Some(Request::GetWiFiCredentials(ssid, uuid, security_type, secret_tx)) => { let s = match NetworkManagerSettings::new(&conn).await { Ok(s) => s, Err(why) => { @@ -460,28 +488,77 @@ async fn start_listening( } }; - // Determine network type - default to PSK for encrypted networks - let mut security_type = NetworkType::PSK; + match security_type { + NetworkType::Open => { + _ = output + .send(Event::WiFiCredentials { + ssid: ssid.clone(), + password: None, + security_type, + }) + .await; + } + t => { + let (tx, rx) = tokio::sync::oneshot::channel(); + let setting_name = if matches!(t, NetworkType::PSK) { + "802-11-wireless-security" + } else { + "802-1x" + }; + let pw_key = if matches!(t, NetworkType::PSK) { + "psk" + } else { + "password" + }; + if let Some(secret_tx) = secret_tx { + let _ = secret_tx + .send(nm_secret_agent::Request::GetSecrets { + setting_name: setting_name.to_string(), + uuid: uuid.to_string(), + resp_tx: tx, + }) + .await; + let _ = tokio::time::timeout(Duration::from_secs(10), async move { + if let Some(password) = + rx.await.ok().and_then(|mut secrets| secrets.remove(pw_key)) + { + _ = output + .send(Event::WiFiCredentials { + ssid: ssid.clone(), + password: Some(password), + security_type, + }) + .await; + } else { + _ = output + .send(Event::WiFiCredentials { + ssid: ssid.clone(), + password: None, + security_type, + }) + .await; + } + }); + } else { + let known_conns = s.list_connections().await.unwrap_or_default(); + for c in known_conns { + let settings = c.get_settings().await.ok().unwrap_or_default(); + let settings_parsed = Settings::new(settings.clone()); - let known_conns = s.list_connections().await.unwrap_or_default(); - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - let settings_parsed = Settings::new(settings.clone()); - - if let Some(saved_ssid) = settings_parsed - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|s| String::from_utf8(s).ok()) - { - if saved_ssid == ssid.as_ref() { - let password = c - .get_secrets("802-11-wireless-security") - .await - .ok() - .and_then(|secrets| { - // Look for PSK password - secrets + if let Some(saved_ssid) = settings_parsed + .wifi + .clone() + .and_then(|w| w.ssid) + .and_then(|s| String::from_utf8(s).ok()) + { + if saved_ssid == ssid.as_ref() { + let password = + c.get_secrets("802-11-wireless-security") + .await + .ok() + .and_then(|secrets| { + // Look for PSK password + secrets .get("802-11-wireless-security") .and_then(|sec| sec.get("psk")) .and_then(|v| { @@ -498,21 +575,19 @@ async fn start_listening( }) .map(|s| s.to_string()) }) - }); + }); - // If no password found, might be open network - if password.is_none() { - security_type = NetworkType::Open; + _ = output + .send(Event::WiFiCredentials { + ssid: ssid.clone(), + password: password.map(SecureString::from), + security_type, + }) + .await; + break; + } + } } - - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password, - security_type, - }) - .await; - break; } } } @@ -568,11 +643,19 @@ async fn attempt_wifi_connection( hw_address: HwAddress, network_type: NetworkType, output: &mut futures::channel::mpsc::Sender, + secret_tx: Option>, ) { let state = NetworkManagerState::new(conn).await.unwrap_or_default(); - let success = if let Err(err) = state - .connect_wifi(conn, ssid.as_ref(), None, None, hw_address) + .connect_wifi( + conn, + ssid.as_ref(), + None, + None, + hw_address, + secret_tx, + network_type, + ) .await { tracing::error!("Failed to connect to access point: {:?}", err); @@ -583,7 +666,7 @@ async fn attempt_wifi_connection( _ = request_response( conn, - Request::SelectAccessPoint(ssid, hw_address, network_type), + Request::SelectAccessPoint(ssid, hw_address, network_type, None), success, ) .then(|event| output.send(event)) @@ -606,15 +689,26 @@ pub enum Request { identity: Option, password: SecureString, hw_address: HwAddress, + secret_tx: Option>, }, /// Get WiFi credentials for a known access point. - GetWiFiCredentials(SSID), + GetWiFiCredentials( + SSID, + UUID, + NetworkType, + Option>, + ), /// Signal to reload the service. Reload, /// Remove a connection profile. Remove(UUID), /// Connect to a known access point. - SelectAccessPoint(SSID, HwAddress, NetworkType), + SelectAccessPoint( + SSID, + HwAddress, + NetworkType, + Option>, + ), /// Toggle airplaine mode. SetAirplaneMode(bool), /// Toggle WiFi enablement. @@ -639,7 +733,7 @@ pub enum Event { ActiveConns, WiFiCredentials { ssid: SSID, - password: Option, + password: Option, security_type: NetworkType, }, } @@ -814,6 +908,8 @@ impl NetworkManagerState { identity: Option<&str>, password: Option<&str>, hw_address: HwAddress, + secret_tx: Option>, + network_type: NetworkType, ) -> Result<(), Error> { let nm = NetworkManager::new(conn).await?; @@ -849,6 +945,7 @@ impl NetworkManagerState { ]), ), ]); + if let Some(identity) = identity { conn_settings.insert( "802-1x", @@ -858,9 +955,14 @@ impl NetworkManagerState { ("eap", Value::Array(vec!["peap"].into())), // most common default ("phase2-auth", Value::Str("mschapv2".into())), - ("password", Value::Str(password.unwrap_or("").into())), ]), ); + if secret_tx.is_none() { + conn_settings + .get_mut("802-1x") + .unwrap() + .insert("password", Value::Str(password.unwrap_or("").into())); + } let wireless = conn_settings.get_mut("802-11-wireless").unwrap(); wireless.insert("security", Value::Str("802-11-wireless-security".into())); wireless.insert("mode", Value::Str("infrastructure".into())); @@ -869,13 +971,11 @@ impl NetworkManagerState { HashMap::from([("key-mgmt", Value::Str("wpa-eap".into()))]), ); } else if let Some(pass) = password { - conn_settings.insert( - "802-11-wireless-security", - HashMap::from([ - ("psk", Value::Str(pass.into())), - ("key-mgmt", Value::Str("wpa-psk".into())), - ]), - ); + let entry = conn_settings.entry("802-11-wireless-security").or_default(); + _ = entry.insert("key-mgmt", Value::Str("wpa-psk".into())); + if secret_tx.is_none() { + _ = entry.insert("psk", Value::Str(pass.into())); + } } let devices = nm.devices().await?; @@ -907,30 +1007,98 @@ impl NetworkManagerState { } } - let active_conn = if let Some(known_conn) = known_conn.as_ref() { - // update settings if needed - if password.is_some() { + let known_conn = if let Some(known_conn) = known_conn { + if secret_tx.is_none() { known_conn.update(conn_settings).await?; } - - nm.activate_connection(known_conn, &device).await? + known_conn } else { - let (_, active_conn) = nm - .add_and_activate_connection(conn_settings, device.inner().path(), &ap.path) - .await?; - let dummy = ActiveConnectionProxy::new(conn, active_conn).await?; - let active = ActiveConnectionProxy::builder(conn) - .destination(dummy.inner().destination().to_owned()) - .unwrap() - .interface(dummy.inner().interface().to_owned()) - .unwrap() - .path(dummy.inner().path().to_owned()) - .unwrap() - .build() - .await - .unwrap(); - ActiveConnection::from(active) + let settings = nm.settings().await?; + let object_path = settings.add_connection(conn_settings).await?; + let known_connection = Connection::from( + ConnectionSettingsProxy::builder(settings.inner().connection()) + .path(object_path)? + .build() + .await?, + ); + let settings = known_connection.get_settings().await?; + let uuid = String::try_from( + settings + .get("connection") + .ok_or_else(|| Error::MissingField("connection"))? + .get("uuid") + .ok_or_else(|| Error::MissingField("uuid"))? + .clone(), + ) + .map_err(|err| zbus::Error::Variant(err))?; + + if let Some((pass, secret_tx)) = password.clone().zip(secret_tx.as_ref()) { + let pass = SecureString::from(pass); + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + + let _ = secret_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: if identity.is_some() { + "802-1x".into() + } else { + "802-11-wireless-security".into() + }, + uuid, + secrets: if identity.is_some() { + HashMap::from([("password".to_string(), pass)]) + } else { + HashMap::from([("psk".to_string(), pass)]) + }, + applied_tx, + }) + .await; + if let Err(err) = applied_rx.await { + tracing::error!("Failed to set secret. {err:?}"); + } + } + + known_connection }; + + if let Some(pass) = password { + let pass = SecureString::from(pass); + if let Some(secret_tx) = secret_tx.as_ref() { + let settings = known_conn.get_settings().await?; + let uuid = String::try_from( + settings + .get("connection") + .ok_or_else(|| Error::MissingField("connection"))? + .get("uuid") + .ok_or_else(|| Error::MissingField("uuid"))? + .clone(), + ) + .map_err(|err| zbus::Error::Variant(err))?; + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + let setting_name: String = if identity.is_some() { + "802-1x".into() + } else { + "802-11-wireless-security".into() + }; + let _ = secret_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name, + uuid, + secrets: if identity.is_some() { + HashMap::from([("password".to_string(), pass)]) + } else { + HashMap::from([("psk".to_string(), pass)]) + }, + applied_tx, + }) + .await; + if let Err(err) = applied_rx.await { + tracing::error!("Failed to set secret. {err:?}"); + } + } + } + + let active_conn = + ActiveConnection::from(nm.activate_connection(&known_conn, &device).await?); let mut changes = active_conn.receive_state_changed().await; _ = tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let mut count = 5; @@ -939,8 +1107,9 @@ impl NetworkManagerState { if let Ok(enums::ActiveConnectionState::Activated) = state { return Ok(()); } else if let Ok(enums::ActiveConnectionState::Deactivated) = state { - return Err(Error::ConnectionActivate); + break; } + if let Ok(Some(s)) = tokio::time::timeout(Duration::from_secs(20), changes.next()).await { @@ -952,9 +1121,47 @@ impl NetworkManagerState { count -= 1; if count <= 0 { - return Err(Error::ConnectionActivate); + break; } } + if let Some(secret_tx) = secret_tx + && !matches!(network_type, NetworkType::Open) + { + let settings = known_conn.get_settings().await?; + let uuid = String::try_from( + settings + .get("connection") + .ok_or_else(|| Error::MissingField("connection"))? + .get("uuid") + .ok_or_else(|| Error::MissingField("uuid"))? + .clone(), + ) + .map_err(|err| zbus::Error::Variant(err))?; + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + let setting_name: String = if identity.is_some() { + "802-1x".into() + } else { + "802-11-wireless-security".into() + }; + if let Err(err) = secret_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name, + uuid, + secrets: Default::default(), + applied_tx, + }) + .await + { + tracing::error!( + "Failed to reset access point secrets after failed activation: {err:?}" + ); + } else if let Err(err) = applied_rx.await { + tracing::error!( + "Failed to reset access point secrets after failed activation: {err:?}" + ); + } + } + return Err(Error::ConnectionActivate); } Err(Error::NoWiFiDevices) diff --git a/subscriptions/network-manager/src/nm_secret_agent.rs b/subscriptions/network-manager/src/nm_secret_agent.rs new file mode 100644 index 0000000..4435cc8 --- /dev/null +++ b/subscriptions/network-manager/src/nm_secret_agent.rs @@ -0,0 +1,740 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, + sync::Arc, +}; + +use bitflags::bitflags; +use cosmic_dbus_networkmanager::interface::settings::connection::ConnectionSettingsProxy; +use futures::{SinkExt, Stream}; +use secure_string::SecureString; +use tokio::sync::oneshot; +use zbus::{ + ObjectServer, fdo, + zvariant::{OwnedValue, Str}, +}; + +pub type SecretSender = Arc>>>; + +pub const SECRET_ID: &'static str = "com.system76.CosmicSettings.NetworkManager"; +pub const DBUS_PATH: &str = "/org/freedesktop/NetworkManager/SecretAgent"; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetSecretsFlags: u32 { + /// No special behavior. + /// By default no user interaction is allowed and secrets must come + /// from persistent storage, otherwise an error is returned. + const NONE = 0x0; + + /// Allows interaction with the user (eg. prompt via UI). + const ALLOW_INTERACTION = 0x1; + + /// Explicitly request new secrets from the user. + /// Implies ALLOW_INTERACTION. + const REQUEST_NEW = 0x2; + + /// Request was initiated by a user action (via D-Bus). + const USER_REQUESTED = 0x4; + + /// Internal flag, not part of the public D-Bus API. + const ONLY_SYSTEM = 0x8000_0000; + + /// Internal flag, not part of the public D-Bus API. + const NO_ERRORS = 0x4000_0000; + } +} + +#[derive(thiserror::Error, Clone, Debug)] +pub enum Error { + #[error("zbus error")] + Zbus(#[from] zbus::Error), + #[error("listening for secret agent closed")] + RecvError(#[from] oneshot::error::RecvError), + #[error("secret service error")] + SecretService(#[from] Arc), + #[error("no password found for identifier: {0}")] + NoPasswordForIdentifier(String), + #[error("utf8 error")] + Utf8Error(#[from] std::string::FromUtf8Error), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PasswordFlag { + /// The system is responsible for providing and storing this secret. + None = 0, + /// A user-session secret agent is responsible for providing and storing + /// this secret; when it is required, agents will be asked to provide it. + AgentOwned = 1, + /// This secret should not be saved but should be requested from the user + /// each time it is required. This flag should be used for One-Time-Pad + /// secrets, PIN codes from hardware tokens, or if the user simply does not + /// want to save the secret. + NotSaved = 2, + /// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user. + NotRequired = 4, +} + +#[derive(Debug, Clone)] +pub struct SecretHint { + pub key: String, + pub message: Option, +} + +fn parse_hints(hints: Vec) -> Vec { + hints + .into_iter() + // fold message hints into previous hints + .fold(Vec::new(), |mut acc, hint| { + if let Some((key, msg)) = hint.split_once(':') { + if let Some(last) = acc.last_mut() { + last.message = Some(format!("{}: {}", key, msg)); + } + } else { + acc.push(SecretHint { + key: hint, + message: None, + }); + } + acc + }) +} + +#[derive(Debug, Clone)] +pub enum Event { + RequestSecret { + uuid: String, + name: String, + description: Option, + previous: SecureString, + tx: SecretSender, + }, + CancelGetSecrets { + uuid: String, + name: String, + }, + Failed(Error), +} + +#[derive(Debug)] +pub enum Request { + SetSecrets { + setting_name: String, + uuid: String, + secrets: HashMap, + applied_tx: oneshot::Sender<()>, + }, + GetSecrets { + setting_name: String, + uuid: String, + resp_tx: oneshot::Sender>, + }, +} + +pub fn secret_agent_stream( + identifier: impl AsRef, + rx: tokio::sync::mpsc::Receiver, +) -> impl Stream { + iced_futures::stream::channel(4, move |mut msg_tx| async move { + if let Err(e) = secret_agent_stream_impl(identifier.as_ref(), msg_tx.clone(), rx).await { + let _ = msg_tx.send(Event::Failed(e)).await; + } + }) +} + +async fn secret_agent_stream_impl( + identifier: &str, + msg_tx: futures::channel::mpsc::Sender, + mut rx: tokio::sync::mpsc::Receiver, +) -> Result<(), Error> { + // register the secret agent with NetworkManager + let proxy = + nm_secret_agent_manager::AgentManagerProxy::builder(&zbus::Connection::system().await?) + .path("/org/freedesktop/NetworkManager/AgentManager")? + .build() + .await?; + + let _ = ObjectServer::at( + proxy.inner().connection().object_server(), + DBUS_PATH, + SettingsSecretAgent { tx: msg_tx }, + ) + .await?; + + proxy.register_with_capabilities(identifier, 1).await?; + + while let Some(request) = rx.recv().await { + match request { + Request::SetSecrets { + setting_name, + uuid, + secrets, + applied_tx, + } => { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|e| Arc::new(e))?; + let collection = ss.get_default_collection().await.map_err(|e| Arc::new(e))?; + + if secrets.is_empty() { + let mut attributes = std::collections::HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", &uuid); + let search_items = collection + .search_items(attributes) + .await + .map_err(|e| Arc::new(e))?; + + for item in &search_items { + item.delete().await.map_err(|e| Arc::new(e))?; + } + let _ = applied_tx.send(()); + + continue; + } + + for (name, secret) in &secrets { + let mut attributes = std::collections::HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", &uuid); + attributes.insert("setting_name", &setting_name); + attributes.insert("name", name); + let _item = collection + .create_item( + "NetworkManager Secret", + attributes, + secret.unsecure().as_bytes(), + true, + "text/plain", + ) + .await + .map_err(|e| Arc::new(e))?; + } + let _ = applied_tx.send(()); + } + Request::GetSecrets { + setting_name, + uuid, + resp_tx, + } => { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|e| Arc::new(e))?; + let collection = ss.get_default_collection().await.map_err(|e| Arc::new(e))?; + + let mut attributes = std::collections::HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", &uuid); + attributes.insert("setting_name", &setting_name); + + let search_items = collection + .search_items(attributes) + .await + .map_err(|e| Arc::new(e))?; + + let mut secrets = HashMap::new(); + for item in &search_items { + let name = item + .get_attributes() + .await + .map_err(|e| Arc::new(e))? + .get("name") + .cloned() + .unwrap_or_else(|| "unknown".to_string()); + let secret = item.get_secret().await.map_err(|e| Arc::new(e))?; + let secret: String = String::from_utf8(secret)?.into(); + secrets.insert(name, SecureString::from(secret)); + } + let _ = resp_tx.send(secrets); + } + } + } + Ok(()) +} + +fn parse_secret_flag(value: &str) -> PasswordFlag { + match value { + "0" => PasswordFlag::None, + "1" => PasswordFlag::AgentOwned, + "2" => PasswordFlag::NotSaved, + "4" => PasswordFlag::NotRequired, + _ => PasswordFlag::AgentOwned, + } +} + +fn setting_has_always_ask(setting: zbus::zvariant::Dict) -> bool { + for (key, value) in setting.iter() { + let Ok(key) = key.downcast_ref::() else { + continue; + }; + let Ok(value) = value.downcast_ref::() else { + continue; + }; + // we only care about "-flags" + if !key.ends_with("-flags") { + continue; + } + + if parse_secret_flag(value.as_str()) == PasswordFlag::NotSaved { + return true; + } + } + + false +} + +fn has_always_ask(setting: Option) -> bool { + setting.map(setting_has_always_ask).unwrap_or(false) +} + +fn is_connection_always_ask(connection: &HashMap>) -> bool { + let conn_setting = match connection.get("connection") { + Some(s) => s, + None => return false, + }; + + let conn_type = match conn_setting + .get("type") + .and_then(|v| v.downcast_ref::().ok()) + { + Some(t) => t, + None => return false, + }; + + // Primary setting (vpn, wifi, ethernet, etc) + if has_always_ask( + connection + .get(&conn_type) + .and_then(|d| d.get("data")) + .and_then(|data| data.downcast_ref::().ok()), + ) { + return true; + } + + match conn_type.as_str() { + "802-11-wireless" => { + if has_always_ask( + connection + .get("802-11-wireless-security") + .and_then(|d| d.get("data")) + .and_then(|data| data.downcast_ref::().ok()), + ) { + return true; + } + if has_always_ask( + connection + .get("802-1x") + .and_then(|d| d.get("data")) + .and_then(|data| data.downcast_ref::().ok()), + ) { + return true; + } + } + + "802-3-ethernet" => { + if has_always_ask( + connection + .get("pppoe") + .and_then(|d| d.get("data")) + .and_then(|data| data.downcast_ref::().ok()), + ) { + return true; + } + if has_always_ask( + connection + .get("802-1x") + .and_then(|d| d.get("data")) + .and_then(|data| data.downcast_ref::().ok()), + ) { + return true; + } + } + + _ => {} + } + + false +} + +#[derive(Debug)] +pub struct SettingsSecretAgent { + tx: futures::channel::mpsc::Sender, +} + +#[zbus::interface(name = "org.freedesktop.NetworkManager.SecretAgent")] +impl SettingsSecretAgent { + /// CancelGetSecrets method + async fn cancel_get_secrets( + &mut self, + connection_path: zbus::zvariant::ObjectPath<'_>, + setting_name: String, + ) -> fdo::Result<()> { + let conn = ConnectionSettingsProxy::builder( + &zbus::Connection::system() + .await + .or_else(|_| Err(fdo::Error::Failed("failed to get uuid".to_string())))?, + ) + .path(connection_path)? + .build() + .await + .map_err(|e| fdo::Error::Failed(e.to_string()))?; + + let uuid = conn + .get_settings() + .await + .map_err(|e| fdo::Error::Failed(e.to_string()))? + .get("connection") + .and_then(|m| m.get("uuid")) + .and_then(|v| v.downcast_ref::().ok()) + .ok_or_else(|| fdo::Error::Failed("failed to get uuid".to_string()))? + .to_string(); + if let Err(e) = self + .tx + .clone() + .send(Event::CancelGetSecrets { + uuid, + name: setting_name, + }) + .await + && e.is_disconnected() + { + return Err(fdo::Error::Failed( + "failed to send cancel message".to_string(), + )); + } + + Ok(()) + } + + /// DeleteSecrets method + async fn delete_secrets( + &self, + connection: HashMap>, + connection_path: zbus::zvariant::ObjectPath<'_>, + ) -> fdo::Result<()> { + match self.delete_secrets_inner(connection, connection_path).await { + Ok(_) => Ok(()), + Err(err) => Err(fdo::Error::Failed(err.to_string())), + } + } + + /// GetSecrets method + async fn get_secrets( + &mut self, + connection: HashMap>, + connection_path: zbus::zvariant::ObjectPath<'_>, + setting_name: String, + hints: Vec, + flags: u32, + ) -> HashMap> { + match self + .get_secrets_inner(connection, connection_path, setting_name, hints, flags) + .await + { + Ok(result) => result, + Err(_) => HashMap::new(), + } + } + + /// SaveSecrets method + async fn save_secrets( + &self, + connection: HashMap>, + _connection_path: zbus::zvariant::ObjectPath<'_>, + ) -> fdo::Result<()> { + match self.save_secrets_inner(connection).await { + Ok(_) => Ok(()), + Err(err) => Err(fdo::Error::Failed(err.to_string())), + } + } +} + +impl SettingsSecretAgent { + pub async fn get_secrets_inner( + &mut self, + connection: HashMap>, + connection_path: zbus::zvariant::ObjectPath<'_>, + setting_name: String, + hints: Vec, + flags: u32, + ) -> Result>, Error> { + let flags = GetSecretsFlags::from_bits_truncate(flags); + + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|e| Arc::new(e))?; + + let collection = ss.get_default_collection().await.map_err(|e| Arc::new(e))?; + + let conn_uuid = connection + .get("connection") + .and_then(|m| m.get("uuid")) + .and_then(|v| v.downcast_ref::().ok()) + .ok_or_else(|| Error::NoPasswordForIdentifier(setting_name.clone()))? + .to_string(); + + let conn = + ConnectionSettingsProxy::builder(&zbus::Connection::system().await.or_else(|_| { + Err(Error::Zbus( + fdo::Error::Failed("failed to get uuid".to_string()).into(), + )) + })?) + .path(connection_path)? + .build() + .await + .map_err(|e| Error::Zbus(fdo::Error::Failed(e.to_string()).into()))?; + let settings = conn.get_settings().await?; + let is_vpn = settings + .get("connection") + .and_then(|m| m.get("type")) + .and_then(|v| v.downcast_ref::().ok()) + .map_or(false, |t| t == "vpn"); + let is_always_ask = is_connection_always_ask(&settings); + + let mut setting_attributes = std::collections::HashMap::new(); + setting_attributes.insert("application", SECRET_ID); + setting_attributes.insert("uuid", &conn_uuid); + setting_attributes.insert("setting_name", &setting_name); + + let mut search_items = collection + .search_items(setting_attributes.clone()) + .await + .map_err(|e| Arc::new(e))?; + let mut result = HashMap::new(); + let mut setting = HashMap::new(); + + if hints.is_empty() { + for item in &search_items { + let name = item + .get_attributes() + .await + .map_err(|e| Arc::new(e))? + .get("name") + .cloned() + .unwrap_or_else(|| "unknown".to_string()); + let secret = item.get_secret().await.map_err(|e| Arc::new(e))?; + let secret: String = String::from_utf8(secret)?.into(); + setting.insert(name, zbus::zvariant::OwnedValue::from(Str::from(secret))); + } + result.insert(setting_name, setting); + return Ok(result); + } else { + let hints = parse_hints(hints); + let mut requested = HashSet::new(); + + for SecretHint { key, message } in &hints { + if requested.contains(key) { + continue; + } + requested.insert(key); + if flags.contains(GetSecretsFlags::REQUEST_NEW) + && flags.contains(GetSecretsFlags::ALLOW_INTERACTION) + || is_always_ask + { + // request the secret via the message channel + let (resp_tx, resp_rx) = oneshot::channel(); + // msg begins after ":" + let actual_hint = message.as_ref().map(|m| { + m.split_once(":") + .map(|(_, msg)| msg.trim().to_string()) + .unwrap_or(m.clone()) + }); + if let Err(e) = self + .tx + .clone() + .send(Event::RequestSecret { + uuid: conn_uuid.clone(), + name: setting_name.clone(), + description: actual_hint.clone(), + previous: String::new().into(), + tx: Arc::new(tokio::sync::Mutex::new(Some(resp_tx))), + }) + .await + && e.is_disconnected() + { + continue; + } else { + if let Ok(secret) = resp_rx.await { + let mut named_attribute = setting_attributes.clone(); + named_attribute.insert("name", key); + let _item = collection + .create_item( + "NetworkManager Secret", + named_attribute, + secret.unsecure().as_bytes(), + true, + "text/plain", + ) + .await + .map_err(|e| Arc::new(e))?; + + setting.insert( + key.clone(), + zbus::zvariant::OwnedValue::from(Str::from(secret.unsecure())), + ); + } + } + } else if !is_always_ask { + let mut pos = None; + let mut pos_with_message = None; + for item in &search_items { + let attributes = item.get_attributes().await.map_err(|e| Arc::new(e))?; + if let Some(value) = attributes.get("name") { + if value == key { + if let Some(saved_message) = attributes.get("message") { + if message.as_ref().is_some_and(|msg| msg == saved_message) { + pos_with_message = Some(item); + } + break; + } else { + pos = Some(item); + } + } + } + } + + if let Some(item) = pos_with_message.or(pos) { + let secret = item.get_secret().await.map_err(|e| Arc::new(e))?; + let secret: String = String::from_utf8(secret)?.into(); + if is_vpn { + // ask anyway, but offer the previous one as a hint + let (resp_tx, resp_rx) = oneshot::channel(); + let actual_hint = message.as_ref().map(|m| { + m.split_once(":") + .map(|(_, msg)| msg.trim().to_string()) + .unwrap_or(m.clone()) + }); + if let Err(e) = self + .tx + .clone() + .send(Event::RequestSecret { + uuid: conn_uuid.clone(), + name: setting_name.clone(), + description: actual_hint.clone(), + previous: SecureString::from(secret.clone()), + tx: Arc::new(tokio::sync::Mutex::new(Some(resp_tx))), + }) + .await + && e.is_disconnected() + { + continue; + } else { + if let Ok(secret) = resp_rx.await { + let mut named_attribute = setting_attributes.clone(); + named_attribute.insert("name", key); + let _item = collection + .create_item( + "NetworkManager Secret", + named_attribute, + secret.unsecure().as_bytes(), + true, + "text/plain", + ) + .await + .map_err(|e| Arc::new(e))?; + + setting.insert( + key.clone(), + zbus::zvariant::OwnedValue::from(Str::from( + secret.unsecure(), + )), + ); + } + } + } else { + setting.insert( + key.clone(), + zbus::zvariant::OwnedValue::from(Str::from(secret)), + ); + } + } + } else { + // can't find the secret, and we can't request it, so we just skip it + continue; + } + } + result.insert(setting_name, setting); + return Ok(result); + } + } + + pub async fn delete_secrets_inner( + &self, + connection: HashMap>, + _connection_path: zbus::zvariant::ObjectPath<'_>, + ) -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|e| Arc::new(e))?; + let collection = ss.get_default_collection().await.map_err(|e| Arc::new(e))?; + + let conn_uuid = connection + .get("connection") + .and_then(|m| m.get("uuid")) + .and_then(|v| v.downcast_ref::().ok()) + .ok_or_else(|| Error::NoPasswordForIdentifier("unknown".to_string()))? + .to_string(); + + let mut attributes = std::collections::HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", &conn_uuid); + + let search_items = collection + .search_items(attributes) + .await + .map_err(|e| Arc::new(e))?; + + for item in &search_items { + item.delete().await.map_err(|e| Arc::new(e))?; + } + Ok(()) + } + + pub async fn save_secrets_inner( + &self, + connection: HashMap>, + ) -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|e| Arc::new(e))?; + let collection = ss.get_default_collection().await.map_err(|e| Arc::new(e))?; + let conn_uuid = connection + .get("connection") + .and_then(|m| m.get("uuid")) + .and_then(|v| v.downcast_ref::().ok()) + .ok_or_else(|| Error::NoPasswordForIdentifier("unknown".to_string()))? + .to_string(); + + let secret: Option<(String, String)> = connection + .get("802-11-wireless-security") + .and_then(|m| m.get("psk")) + .and_then(|v| v.downcast_ref::().ok()) + .map(|password| ("psk".to_string(), password.clone())) + .or_else(|| { + connection + .get("802-1x") + .and_then(|s| s.get("password")) + .and_then(|v| v.downcast_ref::().ok()) + .map(|password| ("802-1x-password".to_string(), password.clone())) + }); + if let Some((name, secret)) = secret { + let mut attributes = std::collections::HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", &conn_uuid); + attributes.insert("setting_name", &name); + let _item = collection + .create_item( + "NetworkManager Secret", + attributes, + secret.as_bytes(), + true, + "text/plain", + ) + .await + .map_err(|e| Arc::new(e))?; + Ok(()) + } else { + Err(Error::NoPasswordForIdentifier("unknown".to_string())) + } + } +}