diff options
| author | Dennis Kobert <dennis@kobert.dev> | 2026-03-11 14:15:45 +0100 |
|---|---|---|
| committer | Dennis Kobert <dennis@kobert.dev> | 2026-03-11 14:16:07 +0100 |
| commit | a9a74679fbec07cf158f2295513e3d4c4adc2666 (patch) | |
| tree | 5dcac28283ef53c2869bdedb9badff0896900bb6 /src-repo | |
| parent | 116bd5a46adc7e9caa27f58b19cafe68d3e085f0 (diff) | |
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.
Diffstat (limited to 'src-repo')
| -rw-r--r-- | src-repo/.gitignore | 1 | ||||
| -rw-r--r-- | src-repo/Cargo.lock | 444 | ||||
| -rw-r--r-- | src-repo/Cargo.toml | 14 | ||||
| -rw-r--r-- | src-repo/LICENSE | 201 | ||||
| -rw-r--r-- | src-repo/README.md | 35 | ||||
| -rw-r--r-- | src-repo/src/lib.rs | 121 | ||||
| -rw-r--r-- | src-repo/src/mode.rs | 7 | ||||
| -rw-r--r-- | src-repo/src/pam_any_conversation.rs | 48 | ||||
| -rw-r--r-- | src-repo/src/pam_client.rs | 166 | ||||
| -rw-r--r-- | src-repo/src/raw_conv.rs | 116 | ||||
| -rw-r--r-- | src-repo/src/un_hide_input.rs | 9 | ||||
| -rw-r--r-- | src-repo/test.sh | 5 |
12 files changed, 1167 insertions, 0 deletions
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 |
