summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDennis Kobert <dennis@kobert.dev>2026-03-11 14:15:45 +0100
committerDennis Kobert <dennis@kobert.dev>2026-03-11 14:16:07 +0100
commita9a74679fbec07cf158f2295513e3d4c4adc2666 (patch)
tree5dcac28283ef53c2869bdedb9badff0896900bb6
parent116bd5a46adc7e9caa27f58b19cafe68d3e085f0 (diff)
Port pam-any from pam-bindings to nonstick crateHEADmain
The pam-bindings crate has a critical bug where pam_try! returns PAM_SUCCESS on error in release builds (anowell/pam-rs#16), causing authentication to always succeed. This replaces it with nonstick and a custom pam_client module with raw PAM FFI for thread-safe conversation forwarding.
-rw-r--r--Cargo.lock333
-rw-r--r--default.nix27
l---------result1
-rw-r--r--src-repo/.gitignore1
-rw-r--r--src-repo/Cargo.lock444
-rw-r--r--src-repo/Cargo.toml14
-rw-r--r--src-repo/LICENSE201
-rw-r--r--src-repo/README.md35
-rw-r--r--src-repo/src/lib.rs121
-rw-r--r--src-repo/src/mode.rs7
-rw-r--r--src-repo/src/pam_any_conversation.rs48
-rw-r--r--src-repo/src/pam_client.rs166
-rw-r--r--src-repo/src/raw_conv.rs116
-rw-r--r--src-repo/src/un_hide_input.rs9
-rw-r--r--src-repo/test.sh5
15 files changed, 1385 insertions, 143 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4ac59f0..e91f854 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,41 +1,41 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "aho-corasick"
-version = "1.1.2"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
-version = "0.69.4"
+version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
- "lazy_static",
- "lazycell",
+ "log",
+ "prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
- "syn 2.0.52",
+ "syn",
]
[[package]]
name = "bitflags"
-version = "2.4.2"
+version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cexpr"
@@ -47,86 +47,98 @@ dependencies = [
]
[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
name = "clang-sys"
-version = "1.7.0"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
+ "libloading",
]
[[package]]
-name = "crossbeam-channel"
-version = "0.5.12"
+name = "either"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
-dependencies = [
- "crossbeam-utils",
-]
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
-name = "crossbeam-utils"
-version = "0.8.19"
+name = "equivalent"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
-name = "either"
-version = "1.10.0"
+name = "glob"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
-name = "glob"
-version = "0.3.1"
+name = "hashbrown"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
-name = "itertools"
-version = "0.12.1"
+name = "indexmap"
+version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
- "either",
+ "equivalent",
+ "hashbrown",
]
[[package]]
-name = "itoa"
-version = "1.0.10"
+name = "itertools"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
[[package]]
-name = "lazy_static"
-version = "1.4.0"
+name = "itoa"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
-name = "lazycell"
-version = "1.3.0"
+name = "libc"
+version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
-name = "libc"
-version = "0.2.153"
+name = "libloading"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
[[package]]
name = "log"
-version = "0.4.21"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
-version = "2.7.1"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "minimal-lexical"
@@ -145,85 +157,94 @@ dependencies = [
]
[[package]]
-name = "pam"
-version = "0.8.0"
+name = "nonstick"
+version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba"
+checksum = "15669b8f01cf3dcad5bae6b2af9d03972657b8e8fca2fef6e43c5c223896d9ff"
dependencies = [
+ "bindgen",
+ "bitflags",
"libc",
- "memchr",
- "pam-macros",
- "pam-sys",
- "users",
+ "num_enum",
+ "secure-string",
+ "thiserror",
]
[[package]]
-name = "pam-any"
-version = "0.1.0"
+name = "num_enum"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
dependencies = [
- "crossbeam-channel",
- "libc",
- "log",
- "pam",
- "pam-bindings",
- "serde",
- "serde_json",
- "termios",
+ "num_enum_derive",
+ "rustversion",
]
[[package]]
-name = "pam-bindings"
-version = "0.1.1"
+name = "num_enum_derive"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95c337e922acb6ab9c3ddd1016fed13957a5bf14f51b6caa293ddc8dd47660ca"
+checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pam-any"
+version = "0.2.0"
dependencies = [
"libc",
+ "nonstick",
+ "serde",
+ "serde_json",
+ "termios",
]
[[package]]
-name = "pam-macros"
-version = "0.0.3"
+name = "prettyplease"
+version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "quote",
- "syn 1.0.109",
+ "syn",
]
[[package]]
-name = "pam-sys"
-version = "1.0.0-alpha5"
+name = "proc-macro-crate"
+version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
- "bindgen",
- "libc",
+ "toml_edit",
]
[[package]]
name = "proc-macro2"
-version = "1.0.79"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.35"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
-version = "1.10.3"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -233,9 +254,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.6"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -244,51 +265,73 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.2"
+version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustc-hash"
-version = "1.1.0"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
-name = "ryu"
-version = "1.0.17"
+name = "rustversion"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "secure-string"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "548ba8c9ff631f7bb3a64de1e8ad73fe20f6d04090724f2b496ed45314ad7488"
+dependencies = [
+ "libc",
+ "zeroize",
+]
[[package]]
name = "serde"
-version = "1.0.197"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.197"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.52",
+ "syn",
]
[[package]]
name = "serde_json"
-version = "1.0.114"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
- "ryu",
+ "memchr",
"serde",
+ "serde_core",
+ "zmij",
]
[[package]]
@@ -299,9 +342,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
-version = "1.0.109"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -309,37 +352,93 @@ dependencies = [
]
[[package]]
-name = "syn"
-version = "2.0.52"
+name = "termios"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "unicode-ident",
+ "syn",
]
[[package]]
-name = "termios"
-version = "0.3.3"
+name = "toml_datetime"
+version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
+checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
- "libc",
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.4+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.9+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
+dependencies = [
+ "winnow",
]
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
-name = "users"
-version = "0.10.0"
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
- "libc",
- "log",
+ "memchr",
]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/default.nix b/default.nix
index 4ae9a74..b9bd421 100644
--- a/default.nix
+++ b/default.nix
@@ -1,20 +1,14 @@
{
lib,
rustPlatform,
- fetchFromGitHub,
pam,
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "pam-any";
- version = "0-unstable-2024-11-20";
+ version = "0.2.0";
- src = fetchFromGitHub {
- owner = "ChocolateLoverRaj";
- repo = "pam-any";
- rev = "e77687709d092a6bb77e57a444403798855075c0";
- hash = "sha256-em/vifkse1Qp3iX/oE1vHQK6UAoJvbhBOowhFhgQ+qw=";
- };
+ src = ./src-repo;
cargoLock = {
lockFile = ./Cargo.lock;
@@ -28,23 +22,6 @@ rustPlatform.buildRustPackage (finalAttrs: {
pam
];
- # Support reading config from a file path instead of only inline JSON,
- # since NixOS appends comments to PAM rule lines which breaks JSON parsing.
- postPatch = ''
- substituteInPlace src/lib.rs \
- --replace-fail \
- 'let arg_string = args.iter().map(|s| s.to_str().unwrap()).collect::<Vec<_>>().join(" ");' \
- 'let arg_string = {
- let joined = args.iter().map(|s| s.to_str().unwrap()).collect::<Vec<_>>().join(" ");
- if joined.starts_with("/") {
- std::fs::read_to_string(joined.split_whitespace().next().unwrap())
- .unwrap_or(joined)
- } else {
- joined
- }
- };'
- '';
-
# cdylib produces no binaries, only libpam_any.so
installPhase = ''
runHook preInstall
diff --git a/result b/result
deleted file mode 120000
index 26e2c1b..0000000
--- a/result
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/455520syawyk79pvc9in9fjq5yxxr7ii-pam-any-0-unstable-2024-11-20 \ No newline at end of file
diff --git a/src-repo/.gitignore b/src-repo/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/src-repo/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/src-repo/Cargo.lock b/src-repo/Cargo.lock
new file mode 100644
index 0000000..e91f854
--- /dev/null
+++ b/src-repo/Cargo.lock
@@ -0,0 +1,444 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "libc"
+version = "0.2.183"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nonstick"
+version = "0.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15669b8f01cf3dcad5bae6b2af9d03972657b8e8fca2fef6e43c5c223896d9ff"
+dependencies = [
+ "bindgen",
+ "bitflags",
+ "libc",
+ "num_enum",
+ "secure-string",
+ "thiserror",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pam-any"
+version = "0.2.0"
+dependencies = [
+ "libc",
+ "nonstick",
+ "serde",
+ "serde_json",
+ "termios",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "secure-string"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "548ba8c9ff631f7bb3a64de1e8ad73fe20f6d04090724f2b496ed45314ad7488"
+dependencies = [
+ "libc",
+ "zeroize",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termios"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.0.0+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.4+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.9+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/src-repo/Cargo.toml b/src-repo/Cargo.toml
new file mode 100644
index 0000000..9c79c58
--- /dev/null
+++ b/src-repo/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "pam-any"
+version = "0.2.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+nonstick = "0.0.7"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+termios = "0.3.3"
+libc = "0.2"
diff --git a/src-repo/LICENSE b/src-repo/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/src-repo/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/src-repo/README.md b/src-repo/README.md
new file mode 100644
index 0000000..b6d45a7
--- /dev/null
+++ b/src-repo/README.md
@@ -0,0 +1,35 @@
+# PAM Any
+A PAM module that runs multiple other PAM modules in parallel, succeeding as long as one of them succeeds.
+
+## Installation
+Install [Rust](https://www.rust-lang.org/learn/get-started)
+
+Build (`cargo build --release`)
+
+Copy the PAM module to the location where PAM modules belong
+```bash
+sudo cp target/release/libpam_any.so /lib64/security
+```
+Depending on the distro, the folder might be `/lib` or `/lib64`. On Fedora it's `/lib64`.
+
+Create / edit a file in `/etc/pam.d`. For example, `/etc/pam.d/sudo` for sudo authentication. Here is an example file:
+```
+auth sufficient libpam_any.so { "mode": "One", "modules": { "login": "Password", "pam-random": "Random Chance" } }
+```
+The text after `libpam_any.so` is a JSON object:
+`mode`: Can be either `"One"` or `"All"`. "One" means that you can authenticate with any of the specified methods. For example, you can *either* type your password or use your fingerprint. "All" means that you must authenticate all of the specified modules, but in any order.
+`modules`: Is an object where the key is a file that exists in `/etc/pam.d` and the value is the display name of the service. In the example above, `pam-any` will internally start PAM authentication based on the `/etc/pam.d/login` file (Text password), and `/etc/pam.d/pam-random` file [(A test module that randomly succeeds / fails)](https://github.com/ChocolateLoverRaj/pam-random). As soon as one of the two modules authenticates successfully, the `pam-any` module will authenticate successfully. If all sub-modules fail (wrong password), then `pam-any` will fail.
+
+## Development
+I created a VM to test stuff without messing up the distro I code in.
+- Create a Fedora VM (can probably be any distro)
+- Create a user named `test`
+- Enable SSH server
+- Enable root password
+- Enable root SSH
+- Setup password-less SSH login
+- Setup [`pam-random`](https://github.com/ChocolateLoverRaj/pam-random) as a 2nd test module
+- Update the `IP` variable in `test.sh`
+- Run `bash ./test.sh`
+- Inside the VM install `pamtester`
+- Inside the VM run `pamtester pam-any test authenticate`
diff --git a/src-repo/src/lib.rs b/src-repo/src/lib.rs
new file mode 100644
index 0000000..a32eb5e
--- /dev/null
+++ b/src-repo/src/lib.rs
@@ -0,0 +1,121 @@
+use std::collections::HashMap;
+use std::ffi::CStr;
+use std::sync::mpsc::channel;
+use std::sync::{Arc, Mutex};
+use std::thread;
+
+use nonstick::handle::PamHandleModule;
+use nonstick::{pam_hooks, ErrorCode, Flags, PamModule, Result as PamResult};
+
+use crate::pam_client::{Client, PamResult as PamClientResult};
+use serde::{Deserialize, Serialize};
+
+use crate::mode::Mode;
+use crate::pam_any_conversation::PamAnyConversation;
+use crate::raw_conv::RawConv;
+use crate::un_hide_input::un_hide_input;
+
+mod mode;
+mod pam_any_conversation;
+mod pam_client;
+mod raw_conv;
+mod un_hide_input;
+
+struct PamAny;
+pam_hooks!(PamAny);
+
+#[derive(Serialize, Deserialize, Debug)]
+struct Input {
+ mode: Mode,
+ modules: HashMap<String, String>,
+}
+
+impl<T: PamHandleModule> PamModule<T> for PamAny {
+ fn authenticate(handle: &mut T, args: Vec<&CStr>, _flags: Flags) -> PamResult<()> {
+ let arg_string = args
+ .iter()
+ .map(|s| s.to_str().unwrap_or(""))
+ .collect::<Vec<_>>()
+ .join(" ");
+
+ // Support reading config from a file path (for NixOS compatibility)
+ let config_string = if arg_string.starts_with('/') {
+ let path = arg_string.split_whitespace().next().unwrap_or("");
+ std::fs::read_to_string(path).map_err(|_| ErrorCode::ServiceError)?
+ } else {
+ arg_string
+ };
+
+ let input: Input =
+ serde_json::from_str(&config_string).map_err(|_| ErrorCode::ServiceError)?;
+
+ let user = handle.get_user(None)?.to_owned();
+
+ // Get the raw PAM conversation to share across threads.
+ let handle_ptr = handle as *mut T as *mut libc::c_void;
+ let conv = RawConv::from_pam_handle(handle_ptr)
+ .ok_or(ErrorCode::ConversationError)?;
+ let conv = Arc::new(Mutex::new(conv));
+
+ let (tx, rx) = channel::<PamClientResult<()>>();
+ let _handles: Vec<_> = input
+ .modules
+ .iter()
+ .map(|(service, service_display_name)| {
+ let service = service.to_owned();
+ let tx = tx.clone();
+ let conv = conv.clone();
+ let user = user.clone();
+ let service_display_name = service_display_name.to_owned();
+ thread::spawn(move || {
+ let client = Client::with_conversation(
+ &service,
+ PamAnyConversation {
+ service_display_name,
+ user,
+ conv,
+ },
+ );
+ let result = match client {
+ Ok(mut c) => c.authenticate(),
+ Err(e) => Err(e),
+ };
+ let _ = tx.send(result);
+ })
+ })
+ .collect();
+
+ match input.mode {
+ Mode::One => {
+ let mut failed_modules = 0;
+ for result in rx {
+ if result.is_ok() {
+ let _ = un_hide_input();
+ return Ok(());
+ } else {
+ failed_modules += 1;
+ if failed_modules == input.modules.len() {
+ return Err(ErrorCode::AuthenticationError);
+ }
+ }
+ }
+ Err(ErrorCode::AuthenticationError)
+ }
+ Mode::All => {
+ let mut successful_modules = 0;
+ for result in rx {
+ if result.is_ok() {
+ successful_modules += 1;
+ if successful_modules == input.modules.len() {
+ let _ = un_hide_input();
+ return Ok(());
+ }
+ } else {
+ return Err(ErrorCode::AuthenticationError);
+ }
+ }
+ Err(ErrorCode::AuthenticationError)
+ }
+ }
+ }
+}
diff --git a/src-repo/src/mode.rs b/src-repo/src/mode.rs
new file mode 100644
index 0000000..8dbfb44
--- /dev/null
+++ b/src-repo/src/mode.rs
@@ -0,0 +1,7 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+pub enum Mode {
+ One,
+ All
+}
diff --git a/src-repo/src/pam_any_conversation.rs b/src-repo/src/pam_any_conversation.rs
new file mode 100644
index 0000000..825cb6a
--- /dev/null
+++ b/src-repo/src/pam_any_conversation.rs
@@ -0,0 +1,48 @@
+use std::ffi::{CStr, CString};
+use std::sync::{Arc, Mutex};
+use std::thread;
+
+use crate::pam_client::Conversation;
+
+use crate::raw_conv::RawConv;
+
+pub struct PamAnyConversation {
+ pub service_display_name: String,
+ pub user: String,
+ pub conv: Arc<Mutex<RawConv>>,
+}
+
+impl Conversation for PamAnyConversation {
+ fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
+ CString::new(self.user.as_str()).map_err(|_| ())
+ }
+
+ fn prompt_blind(&mut self, msg: &CStr) -> Result<CString, ()> {
+ let msg = msg.to_str().map_err(|_| ())?;
+ let conv = self.conv.lock().map_err(|_| ())?;
+ let response = conv.send_prompt(&format!("[{}] {}", self.service_display_name, msg))?;
+ response.ok_or(())
+ }
+
+ fn info(&mut self, msg: &CStr) {
+ let msg = msg.to_str().unwrap_or("");
+ let msg = format!("[{}] {}", self.service_display_name, msg);
+ let conv = self.conv.clone();
+ thread::spawn(move || {
+ if let Ok(conv) = conv.lock() {
+ let _ = conv.send_info(&msg);
+ }
+ });
+ }
+
+ fn error(&mut self, msg: &CStr) {
+ let msg = msg.to_str().unwrap_or("");
+ let msg = format!("[{}] {}", self.service_display_name, msg);
+ let conv = self.conv.clone();
+ thread::spawn(move || {
+ if let Ok(conv) = conv.lock() {
+ let _ = conv.send_error(&msg);
+ }
+ });
+ }
+}
diff --git a/src-repo/src/pam_client.rs b/src-repo/src/pam_client.rs
new file mode 100644
index 0000000..e0fbcc6
--- /dev/null
+++ b/src-repo/src/pam_client.rs
@@ -0,0 +1,166 @@
+use std::ffi::{CStr, CString};
+use std::ptr;
+
+const PAM_PROMPT_ECHO_OFF: libc::c_int = 1;
+const PAM_PROMPT_ECHO_ON: libc::c_int = 2;
+const PAM_ERROR_MSG: libc::c_int = 3;
+const PAM_TEXT_INFO: libc::c_int = 4;
+const PAM_SUCCESS: libc::c_int = 0;
+
+#[repr(C)]
+struct PamMessage {
+ msg_style: libc::c_int,
+ msg: *const libc::c_char,
+}
+
+#[repr(C)]
+struct PamResponse {
+ resp: *mut libc::c_char,
+ resp_retcode: libc::c_int,
+}
+
+type PamConvFn = unsafe extern "C" fn(
+ num_msg: libc::c_int,
+ msg: *mut *const PamMessage,
+ resp: *mut *mut PamResponse,
+ appdata_ptr: *mut libc::c_void,
+) -> libc::c_int;
+
+#[repr(C)]
+struct PamConv {
+ conv: PamConvFn,
+ appdata_ptr: *mut libc::c_void,
+}
+
+type PamHandle = libc::c_void;
+
+extern "C" {
+ fn pam_start(
+ service_name: *const libc::c_char,
+ user: *const libc::c_char,
+ pam_conversation: *const PamConv,
+ pamh: *mut *mut PamHandle,
+ ) -> libc::c_int;
+ fn pam_authenticate(pamh: *mut PamHandle, flags: libc::c_int) -> libc::c_int;
+ fn pam_end(pamh: *mut PamHandle, pam_status: libc::c_int) -> libc::c_int;
+}
+
+pub trait Conversation {
+ fn prompt_echo(&mut self, msg: &CStr) -> Result<CString, ()>;
+ fn prompt_blind(&mut self, msg: &CStr) -> Result<CString, ()>;
+ fn info(&mut self, msg: &CStr);
+ fn error(&mut self, msg: &CStr);
+}
+
+pub struct Client<C: Conversation> {
+ handle: *mut PamHandle,
+ // Box the conversation so the pointer in PamConv remains stable.
+ conversation: Box<C>,
+}
+
+unsafe extern "C" fn converse<C: Conversation>(
+ num_msg: libc::c_int,
+ msg: *mut *const PamMessage,
+ resp: *mut *mut PamResponse,
+ appdata_ptr: *mut libc::c_void,
+) -> libc::c_int {
+ let conv = &mut *(appdata_ptr as *mut C);
+ let count = num_msg as usize;
+
+ let responses = libc::calloc(count, std::mem::size_of::<PamResponse>()) as *mut PamResponse;
+ if responses.is_null() {
+ return libc::ENOMEM;
+ }
+
+ for i in 0..count {
+ let message = &*(*msg.add(i));
+ let c_msg = CStr::from_ptr(message.msg);
+ match message.msg_style {
+ PAM_PROMPT_ECHO_ON => match conv.prompt_echo(c_msg) {
+ Ok(s) => (*responses.add(i)).resp = libc::strdup(s.as_ptr()),
+ Err(_) => {
+ free_responses(responses, count);
+ return PAM_SUCCESS + 1;
+ }
+ },
+ PAM_PROMPT_ECHO_OFF => match conv.prompt_blind(c_msg) {
+ Ok(s) => (*responses.add(i)).resp = libc::strdup(s.as_ptr()),
+ Err(_) => {
+ free_responses(responses, count);
+ return PAM_SUCCESS + 1;
+ }
+ },
+ PAM_TEXT_INFO => conv.info(c_msg),
+ PAM_ERROR_MSG => conv.error(c_msg),
+ _ => {}
+ }
+ }
+
+ *resp = responses;
+ PAM_SUCCESS
+}
+
+unsafe fn free_responses(resp: *mut PamResponse, count: usize) {
+ for i in 0..count {
+ let r = &mut *resp.add(i);
+ if !r.resp.is_null() {
+ libc::free(r.resp as *mut libc::c_void);
+ }
+ }
+ libc::free(resp as *mut libc::c_void);
+}
+
+#[derive(Debug)]
+pub struct PamError(pub libc::c_int);
+
+pub type PamResult<T> = Result<T, PamError>;
+
+impl<C: Conversation> Client<C> {
+ pub fn with_conversation(service: &str, conversation: C) -> PamResult<Self> {
+ let mut conversation = Box::new(conversation);
+ let c_service = CString::new(service).map_err(|_| PamError(1))?;
+
+ let pam_conv = PamConv {
+ conv: converse::<C>,
+ appdata_ptr: &mut *conversation as *mut C as *mut libc::c_void,
+ };
+
+ let mut handle: *mut PamHandle = ptr::null_mut();
+ let ret = unsafe {
+ pam_start(
+ c_service.as_ptr(),
+ ptr::null(),
+ &pam_conv,
+ &mut handle,
+ )
+ };
+
+ if ret != PAM_SUCCESS {
+ return Err(PamError(ret));
+ }
+
+ Ok(Client {
+ handle,
+ conversation,
+ })
+ }
+
+ pub fn authenticate(&mut self) -> PamResult<()> {
+ let ret = unsafe { pam_authenticate(self.handle, 0) };
+ if ret == PAM_SUCCESS {
+ Ok(())
+ } else {
+ Err(PamError(ret))
+ }
+ }
+}
+
+impl<C: Conversation> Drop for Client<C> {
+ fn drop(&mut self) {
+ if !self.handle.is_null() {
+ unsafe {
+ pam_end(self.handle, PAM_SUCCESS);
+ }
+ }
+ }
+}
diff --git a/src-repo/src/raw_conv.rs b/src-repo/src/raw_conv.rs
new file mode 100644
index 0000000..fe93b64
--- /dev/null
+++ b/src-repo/src/raw_conv.rs
@@ -0,0 +1,116 @@
+use std::ffi::{CStr, CString};
+use std::ptr;
+
+const PAM_CONV: libc::c_int = 5;
+const PAM_PROMPT_ECHO_OFF: libc::c_int = 1;
+const PAM_PROMPT_ECHO_ON: libc::c_int = 2;
+const PAM_ERROR_MSG: libc::c_int = 3;
+const PAM_TEXT_INFO: libc::c_int = 4;
+
+#[repr(C)]
+struct PamMessage {
+ msg_style: libc::c_int,
+ msg: *const libc::c_char,
+}
+
+#[repr(C)]
+struct PamResponse {
+ resp: *mut libc::c_char,
+ resp_retcode: libc::c_int,
+}
+
+type PamConvFn = unsafe extern "C" fn(
+ num_msg: libc::c_int,
+ msg: *mut *const PamMessage,
+ resp: *mut *mut PamResponse,
+ appdata_ptr: *mut libc::c_void,
+) -> libc::c_int;
+
+#[repr(C)]
+struct PamConv {
+ conv: PamConvFn,
+ appdata_ptr: *mut libc::c_void,
+}
+
+extern "C" {
+ fn pam_get_item(
+ pamh: *const libc::c_void,
+ item_type: libc::c_int,
+ item: *mut *const libc::c_void,
+ ) -> libc::c_int;
+}
+
+/// Thread-safe wrapper around a raw PAM conversation function pointer.
+pub struct RawConv {
+ conv_fn: PamConvFn,
+ appdata_ptr: *mut libc::c_void,
+}
+
+unsafe impl Send for RawConv {}
+
+impl RawConv {
+ /// Extract the raw PAM conversation from a PAM handle.
+ pub fn from_pam_handle(pamh: *mut libc::c_void) -> Option<Self> {
+ unsafe {
+ let mut conv_ptr: *const libc::c_void = ptr::null();
+ let ret = pam_get_item(pamh, PAM_CONV, &mut conv_ptr);
+ if ret != 0 || conv_ptr.is_null() {
+ return None;
+ }
+ let pam_conv = &*(conv_ptr as *const PamConv);
+ Some(RawConv {
+ conv_fn: pam_conv.conv,
+ appdata_ptr: pam_conv.appdata_ptr,
+ })
+ }
+ }
+
+ /// Send a message through the PAM conversation.
+ /// Returns the response string for prompt types, or None for info/error.
+ pub fn send(&self, msg_style: libc::c_int, msg: &str) -> Result<Option<CString>, ()> {
+ let c_msg = CString::new(msg).map_err(|_| ())?;
+ let pam_msg = PamMessage {
+ msg_style,
+ msg: c_msg.as_ptr(),
+ };
+ let mut msg_ptr: *const PamMessage = &pam_msg;
+ let mut resp_ptr: *mut PamResponse = ptr::null_mut();
+
+ let ret = unsafe { (self.conv_fn)(1, &mut msg_ptr, &mut resp_ptr, self.appdata_ptr) };
+
+ if ret != 0 {
+ return Err(());
+ }
+
+ if resp_ptr.is_null() {
+ return Ok(None);
+ }
+
+ let response = unsafe {
+ let resp = &*resp_ptr;
+ let result = if resp.resp.is_null() {
+ None
+ } else {
+ let s = CStr::from_ptr(resp.resp).to_owned();
+ libc::free(resp.resp as *mut libc::c_void);
+ Some(s)
+ };
+ libc::free(resp_ptr as *mut libc::c_void);
+ result
+ };
+
+ Ok(response)
+ }
+
+ pub fn send_prompt(&self, msg: &str) -> Result<Option<CString>, ()> {
+ self.send(PAM_PROMPT_ECHO_OFF, msg)
+ }
+
+ pub fn send_info(&self, msg: &str) -> Result<Option<CString>, ()> {
+ self.send(PAM_TEXT_INFO, msg)
+ }
+
+ pub fn send_error(&self, msg: &str) -> Result<Option<CString>, ()> {
+ self.send(PAM_ERROR_MSG, msg)
+ }
+}
diff --git a/src-repo/src/un_hide_input.rs b/src-repo/src/un_hide_input.rs
new file mode 100644
index 0000000..6350739
--- /dev/null
+++ b/src-repo/src/un_hide_input.rs
@@ -0,0 +1,9 @@
+use std::io;
+use termios::{ECHO, ICANON, TCSANOW, tcsetattr, Termios};
+
+pub fn un_hide_input() -> io::Result<()> {
+ let mut termios = Termios::from_fd(libc::STDIN_FILENO)?;
+ termios.c_lflag |= ECHO | ICANON;
+ tcsetattr(libc::STDIN_FILENO, TCSANOW, &termios)?;
+ Ok(())
+} \ No newline at end of file
diff --git a/src-repo/test.sh b/src-repo/test.sh
new file mode 100644
index 0000000..f27a9cf
--- /dev/null
+++ b/src-repo/test.sh
@@ -0,0 +1,5 @@
+IP=192.168.124.164
+
+cargo build
+scp ./pam-any root@$IP:/etc/pam.d
+scp ./target/debug/libpam_any.so root@$IP:/lib64/security \ No newline at end of file