Compare commits

...

4 commits

Author SHA1 Message Date
Breadway
ebcc7aa841 Bump version to 0.2.0
All checks were successful
Mirror to GitHub / mirror (push) Successful in 2s
Build and publish package / package (push) Successful in 2m36s
2026-07-19 03:09:14 +08:00
Breadway
39238c25ad Fix identity URLs (Forgejo canonical remote, correct maintainer email) 2026-07-19 03:09:09 +08:00
Breadway
9df32b1917 Remove orphaned bakery.toml; correct README's stale claim
breadlock isn't in bread-ecosystem/registry/bread-ecosystem.toml and
package.yml (the only release workflow this repo has) never reads
bakery.toml — it only touches packaging/arch/PKGBUILD. So the file was
dead metadata, and the README's claim that "bakery.toml is the
bread-ecosystem package index entry" was stale/incorrect: it never
actually was one. breadlock ships pacman-only, which makes sense for a
PAM/greeter component — README now says so explicitly.
2026-07-17 14:03:18 +08:00
Breadway
38aede9126 breadlock: fail-secure dispatch errors, zeroize password material, distinguish PAM config errors, fix output leak and key repeat
- main.rs: dispatch-loop errors no longer silently exit; bounded retry with
  loud logging, and explicitly never call unlock() on an error path (would
  turn a fail-secure crash into a fail-open one)
- Cargo.toml/state.rs/keyboard.rs/auth/*: password buffer is now
  Zeroizing<String>, with explicit zero-then-truncate on backspace and
  zeroize on clear/submit; also zeroes pam-client2's internal Conversation
  copy after use
- state.rs/main.rs: new AuthState::ConfigError, distinct on-screen message
  and error!-level log for PAM context-init failures vs ordinary wrong
  password
- lock/surface.rs/state.rs: LockSurface now tracks its wl_output so
  output_destroyed can remove it, fixing the surfaces Vec leak on
  monitor unplug
- input/keyboard.rs: bind the keyboard via get_keyboard_with_repeat so
  held keys (e.g. backspace) actually repeat, regardless of whether the
  compositor implements server-side wl_keyboard repeat
2026-07-17 06:45:56 +08:00
13 changed files with 270 additions and 133 deletions

174
Cargo.lock generated
View file

@ -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"

View file

@ -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:

View file

@ -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.'",
]

View file

@ -1,9 +1,9 @@
[package]
name = "breadgreet"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
license = "MIT"
authors = ["Breadway <rileyhorsham@gmail.com>"]
authors = ["Breadway <plasticbread849@gmail.com>"]
description = "Graphical greetd greeter for Hyprland / Wayland"
[[bin]]

View file

@ -1,9 +1,9 @@
[package]
name = "breadlock-ui"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
license = "MIT"
authors = ["Breadway <rileyhorsham@gmail.com>"]
authors = ["Breadway <plasticbread849@gmail.com>"]
[dependencies]
bread-theme.workspace = true

View file

@ -1,9 +1,9 @@
[package]
name = "breadlock"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
license = "MIT"
authors = ["Breadway <rileyhorsham@gmail.com>"]
authors = ["Breadway <plasticbread849@gmail.com>"]
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

View file

@ -35,8 +35,14 @@ pub fn register<Data: 'static>(
/// 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<AuthResult>) {
/// conversation consumes it — it is never logged. It's a `Zeroizing<String>`
/// 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<String>,
result_tx: Sender<AuthResult>,
) {
std::thread::spawn(move || {
let result = pam::check(&username, &password);
let _ = result_tx.send(result);

View file

@ -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
}

View file

@ -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());
}
}

View file

@ -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<Self>,
_output: wl_output::WlOutput,
output: wl_output::WlOutput,
) {
self.surfaces.retain(|s| s.output != output);
}
}

View file

@ -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();
}

View file

@ -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<String>,
pub auth_state: AuthState,
pub auth_tx: Sender<AuthResult>,
@ -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);
}

View file

@ -1,11 +1,11 @@
# Maintainer: Breadway <rileyhorsham@gmail.com>
# Maintainer: Breadway <plasticbread849@gmail.com>
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