diff --git a/Cargo.lock b/Cargo.lock index 36b2706..4e04b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,9 +57,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -77,7 +77,7 @@ dependencies = [ [[package]] name = "breadgreet" -version = "0.1.1" +version = "0.2.0" dependencies = [ "bread-theme", "breadlock-ui", @@ -95,7 +95,7 @@ dependencies = [ [[package]] name = "breadlock" -version = "0.1.1" +version = "0.2.0" dependencies = [ "breadlock-ui", "chrono", @@ -108,11 +108,12 @@ dependencies = [ "tracing", "tracing-subscriber", "wayland-client", + "zeroize", ] [[package]] name = "breadlock-ui" -version = "0.1.1" +version = "0.2.0" dependencies = [ "bread-theme", "chrono", @@ -130,18 +131,18 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", @@ -150,9 +151,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cairo-rs" @@ -204,9 +205,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "shlex", @@ -290,9 +291,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "cursor-icon" @@ -401,9 +402,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +checksum = "ad67eced03f5504d9cbd3a879b5958b5c54d4e5fd794361c6eb21b05fb703411" dependencies = [ "bytemuck", ] @@ -442,9 +443,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -457,9 +458,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -467,15 +468,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -484,15 +485,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -501,21 +502,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -939,9 +940,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -973,9 +974,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -1101,7 +1102,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -1145,12 +1146,13 @@ checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "read-fonts" -version = "0.39.2" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", "font-types", + "once_cell", ] [[package]] @@ -1166,9 +1168,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -1251,9 +1253,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rustybuzz" @@ -1280,9 +1282,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -1378,15 +1380,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "skrifa" -version = "0.42.1" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", "read-fonts", @@ -1451,9 +1453,9 @@ checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -1461,9 +1463,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -1476,9 +1478,9 @@ checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" [[package]] name = "swash" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0811b01ca2c4e8718760713911feaf4675c24f94e50530a015ec646cfb622f7c" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" dependencies = [ "skrifa", "yazi", @@ -1487,9 +1489,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -1514,7 +1516,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "version-compare", ] @@ -1566,9 +1568,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] @@ -1601,9 +1603,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -1616,9 +1618,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -1632,9 +1634,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", @@ -1655,9 +1657,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", "serde_core", @@ -1665,7 +1667,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1702,14 +1704,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1718,7 +1720,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1729,9 +1731,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -2196,9 +2198,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -2248,7 +2250,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] -name = "zmij" -version = "1.0.21" +name = "zeroize" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/README.md b/README.md index 7ef28da..3b49969 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Two binaries, one workspace: - **`breadlock`** — locks the *already running* Hyprland session via `ext-session-lock-v1`. Drop-in for `hyprlock`. - **`breadgreet`** — a graphical greeter that speaks `greetd`'s own IPC protocol (the same architecture as `gtkgreet`/`regreet`). `greetd` keeps owning PAM auth, VT switching, and session launching; `breadgreet` only draws the login UI and relays the conversation. This is a deliberate choice over reimplementing a display manager from scratch — `greetd` is already installed and battle-tested. -Both use [`bread-theme`](https://github.com/Breadway/bread-ecosystem) for palette loading, matching the rest of the bread* ecosystem (breadbar, breadbox, bos-settings). +Both use [`bread-theme`](https://git.breadway.dev/Breadway/bread-ecosystem) for palette loading, matching the rest of the bread* ecosystem (breadbar, breadbox, bos-settings). ## Architecture @@ -54,7 +54,7 @@ sudo pacman -S gtk4 wayland libxkbcommon pam rust cargo ## Packaging -`packaging/arch/PKGBUILD` builds and installs both binaries plus `/etc/pam.d/breadlock`. `bakery.toml` is the bread-ecosystem package index entry. +`packaging/arch/PKGBUILD` builds and installs both binaries plus `/etc/pam.d/breadlock`, published to the `[breadway]` pacman repo by `.forgejo/workflows/package.yml`. breadlock is pacman-only — it is not in bread-ecosystem's registry and has no `bakery.toml`; a PAM/greeter component gets installed through the package manager, not the bakery curl-script channel. **Not included, by design**: this repo does not touch `/etc/greetd/config.toml`, install a lock keybind, or wire up `hypridle`. Once packaged, wiring BOS to actually use these binaries means: diff --git a/bakery.toml b/bakery.toml deleted file mode 100644 index 4674b71..0000000 --- a/bakery.toml +++ /dev/null @@ -1,15 +0,0 @@ -name = "breadlock" -description = "Session locker and greetd greeter for Hyprland / Wayland" -binaries = ["breadlock", "breadgreet"] -system_deps = ["pam", "wayland", "libxkbcommon", "gtk4"] -optional_system_deps = ["cage", "hyprland"] -bread_deps = [] - -[config] -dir = "~/.config/breadlock" -example = "breadlock.example.toml" - -[install] -post_install = [ - "echo 'breadlock installed. /etc/pam.d/breadlock is installed by the package; wiring greetd (cage -s -- breadgreet) and a lock keybind/hypridle is a separate manual step.'", -] diff --git a/breadgreet/Cargo.toml b/breadgreet/Cargo.toml index d7e727f..5496a9a 100644 --- a/breadgreet/Cargo.toml +++ b/breadgreet/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "breadgreet" -version = "0.1.1" +version = "0.2.0" edition = "2021" license = "MIT" -authors = ["Breadway "] +authors = ["Breadway "] description = "Graphical greetd greeter for Hyprland / Wayland" [[bin]] diff --git a/breadlock-ui/Cargo.toml b/breadlock-ui/Cargo.toml index 1b8aa33..10fe27c 100644 --- a/breadlock-ui/Cargo.toml +++ b/breadlock-ui/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "breadlock-ui" -version = "0.1.1" +version = "0.2.0" edition = "2021" license = "MIT" -authors = ["Breadway "] +authors = ["Breadway "] [dependencies] bread-theme.workspace = true diff --git a/breadlock/Cargo.toml b/breadlock/Cargo.toml index aed348c..003840a 100644 --- a/breadlock/Cargo.toml +++ b/breadlock/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "breadlock" -version = "0.1.1" +version = "0.2.0" edition = "2021" license = "MIT" -authors = ["Breadway "] +authors = ["Breadway "] description = "Session locker for Hyprland / Wayland (ext-session-lock-v1)" [[bin]] @@ -24,6 +24,7 @@ wayland-client = "0.31" tiny-skia = "0.12" chrono = "0.4" pam-client2 = { version = "0.5", default-features = false } +zeroize = { version = "1", features = ["std"] } serde.workspace = true toml.workspace = true tracing.workspace = true diff --git a/breadlock/src/auth/mod.rs b/breadlock/src/auth/mod.rs index 49eaf40..d1fad8d 100644 --- a/breadlock/src/auth/mod.rs +++ b/breadlock/src/auth/mod.rs @@ -35,8 +35,14 @@ pub fn register( /// Spawns a PAM check for `username`/`password` on its own thread; the /// outcome arrives later as an event on the loop registered via /// [`register`]. `password` is moved in and dropped as soon as the PAM -/// conversation consumes it — it is never logged. -pub fn spawn_check(username: String, password: String, result_tx: Sender) { +/// conversation consumes it — it is never logged. It's a `Zeroizing` +/// so the buffer is wiped the moment it goes out of scope at the end of this +/// closure, rather than just deallocated with the bytes intact. +pub fn spawn_check( + username: String, + password: zeroize::Zeroizing, + result_tx: Sender, +) { std::thread::spawn(move || { let result = pam::check(&username, &password); let _ = result_tx.send(result); diff --git a/breadlock/src/auth/pam.rs b/breadlock/src/auth/pam.rs index c41d9cc..e245dba 100644 --- a/breadlock/src/auth/pam.rs +++ b/breadlock/src/auth/pam.rs @@ -4,6 +4,7 @@ use pam_client2::conv_mock::Conversation; use pam_client2::{Context, Flag}; +use zeroize::Zeroize; /// The PAM service name — matches `/etc/pam.d/breadlock` /// (packaging/pam.d/breadlock), which is what actually determines the auth @@ -24,12 +25,24 @@ pub enum AuthError { /// `acct_mgmt` (no `open_session` — the graphical session is already open; /// this only re-proves who's sitting at the keyboard). pub fn check(username: &str, password: &str) -> Result<(), AuthError> { + // `Conversation::with_credentials` copies `password` into its own + // `String` field (it has to — PAM's conversation callback is invoked + // later, synchronously, by libpam via FFI). That struct has no Drop/ + // zeroize of its own, so we reach back in and zero it explicitly below + // before `ctx` (and the conversation it owns) is dropped. let conv = Conversation::with_credentials(username, password); let mut ctx = Context::new(SERVICE, Some(username), conv).map_err(|_| AuthError::ContextInit)?; - ctx.authenticate(Flag::NONE) - .map_err(|_| AuthError::Authenticate)?; - ctx.acct_mgmt(Flag::NONE) - .map_err(|_| AuthError::AccountInvalid)?; - Ok(()) + + let result = ctx + .authenticate(Flag::NONE) + .map_err(|_| AuthError::Authenticate) + .and_then(|()| { + ctx.acct_mgmt(Flag::NONE) + .map_err(|_| AuthError::AccountInvalid) + }); + + ctx.conversation_mut().password.zeroize(); + + result } diff --git a/breadlock/src/input/keyboard.rs b/breadlock/src/input/keyboard.rs index 75c6c74..0add42b 100644 --- a/breadlock/src/input/keyboard.rs +++ b/breadlock/src/input/keyboard.rs @@ -4,6 +4,7 @@ use smithay_client_toolkit::seat::keyboard::{ use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; use wayland_client::protocol::{wl_keyboard, wl_seat, wl_surface}; use wayland_client::{Connection, QueueHandle}; +use zeroize::Zeroize; use crate::auth; use crate::state::{AppState, AuthState}; @@ -23,7 +24,27 @@ impl SeatHandler for AppState { capability: Capability, ) { if capability == Capability::Keyboard && self.keyboard.is_none() { - match self.seat_state.get_keyboard(qh, &seat, None) { + // Plain `get_keyboard` never populates SCTK's internal repeat + // timer, so `KeyboardHandler::repeat_key` below only ever fires + // for compositors that implement server-side key repeat + // (wl_keyboard >= v10's "repeated" pseudo key-state) themselves — + // Hyprland does not reliably do this. `get_keyboard_with_repeat` + // registers SCTK's own client-side repeat timer driven by the + // compositor's `repeat_info` (delay/rate); if a compositor *does* + // do server-side repeat it advertises `rate = 0`, which this + // timer already treats as disabled, so the two mechanisms can't + // double-fire. + let repeat_qh = qh.clone(); + let loop_handle = self.loop_handle.clone(); + match self.seat_state.get_keyboard_with_repeat( + qh, + &seat, + None, + loop_handle, + Box::new(move |state: &mut AppState, _keyboard, event| { + state.handle_key(&repeat_qh, event); + }), + ) { Ok(keyboard) => self.keyboard = Some(keyboard), Err(err) => tracing::error!(%err, "failed to bind keyboard"), } @@ -127,11 +148,26 @@ impl AppState { match event.keysym { Keysym::Return | Keysym::KP_Enter => self.submit(), Keysym::BackSpace => { - self.password.pop(); + if let Some((idx, _)) = self.password.char_indices().last() { + // Plain `String::pop()` shrinks the logical length but + // leaves the removed character's bytes sitting in the + // buffer's spare capacity. Zero them explicitly before + // truncating. + // + // SAFETY: `idx` comes from `char_indices()`, so it is a + // valid char boundary; the retained prefix `[..idx]` + // is untouched and still valid UTF-8, and we truncate to + // exactly that boundary immediately after zeroing the + // (now-discarded) tail. + unsafe { + self.password.as_mut_vec()[idx..].zeroize(); + } + self.password.truncate(idx); + } self.clear_failed_state(); } Keysym::Escape => { - self.password.clear(); + self.password.zeroize(); self.clear_failed_state(); } _ => { @@ -151,7 +187,7 @@ impl AppState { } fn clear_failed_state(&mut self) { - if self.auth_state == AuthState::Failed { + if matches!(self.auth_state, AuthState::Failed | AuthState::ConfigError) { self.auth_state = AuthState::Idle; } } @@ -161,7 +197,15 @@ impl AppState { return; } self.auth_state = AuthState::Checking; - let password = std::mem::take(&mut self.password); + // Hand ownership of the buffer to the auth thread; re-reserve + // capacity up front so the next password typed doesn't reallocate + // (see the `password` field doc in state.rs). The taken buffer is + // zeroized automatically when it's dropped at the end of the PAM + // check (`auth::spawn_check`/`pam::check`). + let password = std::mem::replace( + &mut self.password, + zeroize::Zeroizing::new(String::with_capacity(128)), + ); auth::spawn_check(self.username.clone(), password, self.auth_tx.clone()); } } diff --git a/breadlock/src/lock/surface.rs b/breadlock/src/lock/surface.rs index 691a840..bb6d520 100644 --- a/breadlock/src/lock/surface.rs +++ b/breadlock/src/lock/surface.rs @@ -73,6 +73,7 @@ impl OutputHandler for AppState { let lock_surface = session_lock.create_lock_surface(surface, &output, qh); self.surfaces.push(LockSurface { surface: lock_surface, + output, width: 0, height: 0, }); @@ -86,11 +87,16 @@ impl OutputHandler for AppState { ) { } + /// A monitor disappeared (unplug, or Hyprland dropping/recreating it on + /// a mode change). Drop the lock surface tied to it — otherwise + /// `surfaces` only ever grows across hotplug cycles and `redraw_all` + /// keeps trying to commit to a surface whose output is gone. fn output_destroyed( &mut self, _conn: &Connection, _qh: &QueueHandle, - _output: wl_output::WlOutput, + output: wl_output::WlOutput, ) { + self.surfaces.retain(|s| s.output != output); } } diff --git a/breadlock/src/main.rs b/breadlock/src/main.rs index e351b1a..484b06a 100644 --- a/breadlock/src/main.rs +++ b/breadlock/src/main.rs @@ -57,8 +57,24 @@ fn main() { state.exit = true; } Err(err) => { - tracing::warn!(%err, "authentication failed"); - state.auth_state = AuthState::Failed; + match err { + // A broken PAM setup (missing/invalid /etc/pam.d/breadlock, + // context init failure) is a config problem, not a typo — + // rendering it identically to "wrong password" would lock + // the user out with zero indication of what's actually + // wrong. Log loudly and show a distinct on-screen message. + auth::AuthError::ContextInit => { + tracing::error!( + %err, + "PAM context initialization failed — check /etc/pam.d/breadlock exists and is valid; authentication cannot succeed until this is fixed" + ); + state.auth_state = AuthState::ConfigError; + } + auth::AuthError::Authenticate | auth::AuthError::AccountInvalid => { + tracing::warn!(%err, "authentication failed"); + state.auth_state = AuthState::Failed; + } + } state.schedule_clear_failed(auth_result_qh.clone()); } } @@ -88,7 +104,10 @@ fn main() { background, text_renderer: breadlock_ui::painter::TextRenderer::new(), username, - password: String::new(), + // Pre-reserve capacity so ordinary typing doesn't reallocate — a + // reallocation leaves the old (unzeroized) backing buffer, with the + // password bytes still in it, on the heap. + password: zeroize::Zeroizing::new(String::with_capacity(128)), auth_state: AuthState::Idle, auth_tx, exit: false, @@ -107,6 +126,7 @@ fn main() { let lock_surface = session_lock.create_lock_surface(surface, &output, &qh); app_state.surfaces.push(LockSurface { surface: lock_surface, + output, width: 0, height: 0, }); @@ -129,15 +149,48 @@ fn main() { ) .expect("failed to register the clock-tick timer"); + // A dispatch error here is the one path that can end this process while + // the session lock is still up: `SessionLockInner::drop` deliberately + // does *not* send `unlock`, only `destroy` (see the crate's own doc + // comment — "choosing not to unlock here results in us failing secure"), + // so an abrupt exit stays fail-secure at the protocol level; the failure + // mode is a frozen/unusable lock screen (Hyprland's "lock client + // crashed" state), not an unlocked one. We do NOT call `.unlock()` from + // here — doing so on an error path would make an unattended failure + // capable of unlocking the session, i.e. turn a fail-secure bug into a + // fail-open one. Instead: tolerate a burst of transient errors (a single + // `dispatch()` hiccup shouldn't be fatal) and only give up, loudly, after + // several consecutive failures. + const MAX_CONSECUTIVE_DISPATCH_ERRORS: u32 = 5; + let mut consecutive_errors = 0u32; while !app_state.exit { - if let Err(err) = event_loop.dispatch(Duration::from_millis(250), &mut app_state) { - tracing::error!(%err, "event loop dispatch failed"); - break; + match event_loop.dispatch(Duration::from_millis(250), &mut app_state) { + Ok(()) => consecutive_errors = 0, + Err(err) => { + consecutive_errors += 1; + tracing::error!( + %err, + consecutive_errors, + "event loop dispatch failed — session remains locked (fail-secure); \ + if this persists the lock screen may become unresponsive and require \ + a VT switch or `loginctl` to recover" + ); + if consecutive_errors >= MAX_CONSECUTIVE_DISPATCH_ERRORS { + tracing::error!( + "giving up after {consecutive_errors} consecutive dispatch failures; \ + exiting WITHOUT unlocking — this is intentional (fail-secure), but \ + the screen will likely be stuck and need a VT switch to recover" + ); + break; + } + } } } // Make sure the compositor actually receives the unlock/destroy - // requests queued above before the process exits. + // requests queued above (from a successful auth) before the process + // exits. This is a no-op if we got here via the dispatch-error path + // above, since nothing queued an unlock in that case. let _ = app_state.conn.roundtrip(); } diff --git a/breadlock/src/state.rs b/breadlock/src/state.rs index 0dc852e..8726cc9 100644 --- a/breadlock/src/state.rs +++ b/breadlock/src/state.rs @@ -9,7 +9,7 @@ use smithay_client_toolkit::seat::SeatState; use smithay_client_toolkit::session_lock::{SessionLock, SessionLockState, SessionLockSurface}; use smithay_client_toolkit::shm::{Shm, ShmHandler}; use std::time::Duration; -use wayland_client::protocol::{wl_keyboard, wl_shm}; +use wayland_client::protocol::{wl_keyboard, wl_output, wl_shm}; use wayland_client::{Connection, QueueHandle}; use crate::auth::AuthResult; @@ -18,9 +18,12 @@ use crate::config::Config; use crate::render; /// Per-output lock surface plus the size the compositor last `configure`d it -/// to (0x0 until the first configure arrives). +/// to (0x0 until the first configure arrives). `output` is kept so +/// `output_destroyed` can find and drop the surface belonging to an unplugged +/// monitor — without it, hotplug/unplug cycles only ever grow `surfaces`. pub struct LockSurface { pub surface: SessionLockSurface, + pub output: wl_output::WlOutput, pub width: u32, pub height: u32, } @@ -31,7 +34,16 @@ pub enum AuthState { /// A PAM check is running on its own thread; input is ignored until it /// resolves so a second Enter can't race the first attempt. Checking, + /// The password (or account state) was rejected by PAM — an ordinary + /// wrong-password/locked-account outcome the user can retry. Failed, + /// PAM itself failed to initialize (e.g. `/etc/pam.d/breadlock` is + /// missing or unreadable) — this is a config/deployment problem, not + /// something the user's password can fix. Rendered with a distinct + /// message so a broken install doesn't look like an endless string of + /// typos with no way to discover the real cause. See `main.rs`'s + /// auth-result callback, which is the only place this is set. + ConfigError, } pub struct AppState { @@ -53,7 +65,13 @@ pub struct AppState { pub text_renderer: breadlock_ui::painter::TextRenderer, pub username: String, - pub password: String, + /// Wrapped in `Zeroizing` so the buffer is wiped on every drop/replace + /// (e.g. when `submit()` swaps in a fresh one) rather than just + /// deallocated with the password bytes left sitting in freed heap + /// memory. Individual edits (backspace, clear) still need their own + /// explicit zeroing — see `input/keyboard.rs` — since `Zeroizing` only + /// hooks `Drop`, not in-place mutation. + pub password: zeroize::Zeroizing, pub auth_state: AuthState, pub auth_tx: Sender, @@ -81,6 +99,9 @@ impl AppState { let status_text = match self.auth_state { AuthState::Checking => Some("Checking…".to_string()), AuthState::Failed => Some("Wrong password".to_string()), + AuthState::ConfigError => { + Some("PAM config error — check logs (breadlock service not set up correctly)".to_string()) + } AuthState::Idle => None, }; @@ -92,7 +113,7 @@ impl AppState { font_family: &self.config.appearance.font.family, clock_text: &clock_text, password_len: self.password.len(), - failed: self.auth_state == AuthState::Failed, + failed: matches!(self.auth_state, AuthState::Failed | AuthState::ConfigError), status_text: status_text.as_deref(), }; @@ -151,7 +172,7 @@ impl AppState { let _ = self.loop_handle .insert_source(Timer::from_duration(timeout), move |_, _, state| { - if state.auth_state == AuthState::Failed { + if matches!(state.auth_state, AuthState::Failed | AuthState::ConfigError) { state.auth_state = AuthState::Idle; state.redraw_all(&qh); } diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 2b5768d..94db441 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,11 +1,11 @@ -# Maintainer: Breadway +# Maintainer: Breadway pkgname=breadlock pkgver=0.1.0 pkgrel=1 pkgdesc="Session locker and greetd greeter for Hyprland / Wayland" arch=('x86_64') -url="https://github.com/Breadway/breadlock" +url="https://git.breadway.dev/Breadway/breadlock" license=('MIT') # Some Rust deps build vendored C/asm into static archives; makepkg's default # -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, causing