From 11cdd419e56727a8ae7bf400bfa5b48e1f4b100a Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Thu, 12 Mar 2026 22:14:57 +0100 Subject: [PATCH 1/8] feat: add biometric user verification with pluggable providers Implement a pluggable user verification system for FIDO2 operations: - Add UserVerificationProvider trait with priority-based chain - Implement FprintdProvider for fingerprint via D-Bus - Implement FaceIdProvider for webcam face recognition (feature-gated) - Implement NotificationProvider as desktop notification fallback - Implement CommandProvider for custom verification scripts - Add UV configuration with provider selection and per-provider settings - Integrate UV manager into authenticator callbacks - Add comprehensive unit tests (57 tests with face feature) --- Cargo.lock | 2898 +++++++++++++++++-- Cargo.toml | 8 + cmd/passless/Cargo.toml | 13 + cmd/passless/src/authenticator.rs | 309 +- cmd/passless/src/commands/custom.rs | 12 +- cmd/passless/src/main.rs | 8 +- cmd/passless/src/notification.rs | 1 + cmd/passless/src/uv/command.rs | 258 ++ cmd/passless/src/uv/face.rs | 447 +++ cmd/passless/src/uv/fprintd.rs | 317 ++ cmd/passless/src/uv/manager.rs | 319 ++ cmd/passless/src/uv/mod.rs | 213 ++ cmd/passless/src/uv/notification.rs | 164 ++ docs/adr/001-biometric-user-verification.md | 104 + passless-core/src/config.rs | 198 ++ 15 files changed, 4957 insertions(+), 312 deletions(-) create mode 100644 cmd/passless/src/uv/command.rs create mode 100644 cmd/passless/src/uv/face.rs create mode 100644 cmd/passless/src/uv/fprintd.rs create mode 100644 cmd/passless/src/uv/manager.rs create mode 100644 cmd/passless/src/uv/mod.rs create mode 100644 cmd/passless/src/uv/notification.rs create mode 100644 docs/adr/001-biometric-user-verification.md diff --git a/Cargo.lock b/Cargo.lock index 80959127..90a06276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -18,7 +24,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cipher", "cpufeatures", ] @@ -46,6 +52,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -135,6 +159,47 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -180,13 +245,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", - "cfg-if", + "cfg-if 1.0.4", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -214,10 +279,10 @@ dependencies = [ "async-signal", "async-task", "blocking", - "cfg-if", + "cfg-if 1.0.4", "event-listener", "futures-lite", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -240,10 +305,10 @@ dependencies = [ "async-io", "async-lock", "atomic-waker", - "cfg-if", + "cfg-if 1.0.4", "futures-core", "futures-io", - "rustix", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -289,6 +354,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -313,18 +421,68 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", + "which 4.4.2", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitfield" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -365,12 +523,67 @@ dependencies = [ "piper", ] +[[package]] +name = "bon" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cbc" version = "0.1.2" @@ -401,6 +614,21 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.4" @@ -450,6 +678,17 @@ dependencies = [ "inout", ] +[[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 = "clap" version = "4.6.0" @@ -521,6 +760,40 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-foundation 0.10.1", + "core-graphics-types", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -536,6 +809,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -563,107 +849,248 @@ dependencies = [ ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "cookie" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] [[package]] -name = "core2" -version = "0.4.0" +name = "cookie_store" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" dependencies = [ - "memchr", + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "core-foundation" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" dependencies = [ + "core-foundation-sys 0.7.0", "libc", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] [[package]] -name = "crunchy" -version = "0.2.4" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] [[package]] -name = "crypto-bigint" -version = "0.5.5" +name = "core-foundation-sys" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] -name = "crypto-common" -version = "0.1.7" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "ctr" -version = "0.9.2" +name = "core-graphics" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" dependencies = [ - "cipher", + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types", + "libc", ] [[package]] -name = "ctrlc" -version = "3.5.2" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "dispatch2", - "nix 0.31.2", - "windows-sys 0.61.2", + "bitflags 2.11.0", + "core-foundation 0.10.1", + "libc", ] [[package]] -name = "curve25519-dalek" -version = "4.1.3" +name = "core-media-sys" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", ] [[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" +name = "core-video-sys" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" dependencies = [ - "proc-macro2", + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics", + "libc", + "metal", + "objc", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix 0.31.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", "quote", "syn 2.0.117", ] @@ -752,7 +1179,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -761,7 +1188,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -778,6 +1205,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.16.9" @@ -816,6 +1258,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -831,12 +1279,27 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if 1.0.4", +] + [[package]] name = "endi" version = "1.1.1" @@ -874,6 +1337,12 @@ dependencies = [ "regex", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "env_logger" version = "0.11.9" @@ -887,6 +1356,26 @@ dependencies = [ "log", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -900,7 +1389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -924,19 +1413,82 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half 2.7.1", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "face_id" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103f0e099482836fb5d2b5cdb987d177333c9b03157602d6a1c2061ee47d448c" +dependencies = [ + "bon", + "hdbscan", + "hf-hub", + "image", + "nalgebra", + "ndarray", + "ort", + "rayon", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -952,6 +1504,49 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -961,12 +1556,48 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -987,25 +1618,67 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "futures-macro" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ - "typenum", - "version_check", - "zeroize", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "futures-sink" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] @@ -1014,7 +1687,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "libc", "r-efi", "wasip2", @@ -1030,6 +1703,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "git-state" version = "0.1.0" @@ -1042,13 +1725,115 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", "url", ] +[[package]] +name = "glam" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" + +[[package]] +name = "glam" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b" + +[[package]] +name = "glam" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" + +[[package]] +name = "glam" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776" + +[[package]] +name = "glam" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34" + +[[package]] +name = "glam" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca" + +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" + +[[package]] +name = "glam" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" + +[[package]] +name = "glam" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" + +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + +[[package]] +name = "glam" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -1056,10 +1841,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "1.8.3" @@ -1072,7 +1876,7 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "crunchy", "zerocopy", ] @@ -1083,6 +1887,16 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hdbscan" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "466aa8bfaa22c81efcf67c84e6ad046de13a05c6cc9d41a5094361bff464247a" +dependencies = [ + "kdtree", + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" @@ -1110,6 +1924,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hf-hub" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef3982638978efa195ff11b305f51f1f22f4f0a6cabee7af79b383ebee6a213" +dependencies = [ + "dirs", + "futures", + "http", + "indicatif", + "libc", + "log", + "native-tls", + "num_cpus", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "ureq", + "windows-sys 0.61.2", +] + [[package]] name = "hidapi" version = "2.6.5" @@ -1117,7 +1955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1b71e1f4791fb9e93b9d7ee03d70b501ab48f6151432fbcadeabc30fe15396e" dependencies = [ "cc", - "cfg-if", + "cfg-if 1.0.4", "libc", "pkg-config", "windows-sys 0.61.2", @@ -1141,12 +1979,145 @@ dependencies = [ "digest", ] +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hostname-validator" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1154,7 +2125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "iana-time-zone-haiku", "js-sys", "log", @@ -1279,6 +2250,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "2.13.0" @@ -1289,6 +2300,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "inout" version = "0.1.4" @@ -1299,6 +2323,33 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_debug" version = "1.1.0" @@ -1311,6 +2362,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1361,18 +2421,50 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kdtree" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0a0e9f770b65bac9aad00f97a67ab5c5319effed07f6da385da3c2115e47ba" +dependencies = [ + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libgit2-sys" version = "0.18.3+1.9.2" @@ -1385,6 +2477,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if 1.0.4", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.14" @@ -1406,6 +2508,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1419,81 +2527,405 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] -name = "lock_api" -version = "0.4.14" +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lzma-rust2" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" + +[[package]] +name = "mac-notification-sys" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if 1.0.4", + "rayon", +] + +[[package]] +name = "mbox" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de" +dependencies = [ + "libc", + "stable_deref_trait", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa", + "core-graphics", + "foreign-types", + "log", + "objc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "mozjpeg" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + +[[package]] +name = "nalgebra" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f" +dependencies = [ + "approx", + "glam 0.14.0", + "glam 0.15.2", + "glam 0.16.0", + "glam 0.17.3", + "glam 0.18.0", + "glam 0.19.0", + "glam 0.20.5", + "glam 0.21.3", + "glam 0.22.0", + "glam 0.23.0", + "glam 0.24.2", + "glam 0.25.0", + "glam 0.27.0", + "glam 0.28.0", + "glam 0.29.3", + "glam 0.30.10", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "nasm-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149" +dependencies = [ + "jobserver", + "log", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if 1.0.4", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "scopeguard", + "bitflags 2.11.0", + "cfg-if 1.0.4", + "cfg_aliases", + "libc", ] [[package]] -name = "log" -version = "0.4.29" +name = "nokhwa" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "c4cae50786bfa1214ed441f98addbea51ca1b9aaa9e4bf5369cda36654b3efaa" +dependencies = [ + "flume", + "image", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "paste", + "thiserror 2.0.18", +] [[package]] -name = "mac-notification-sys" -version = "0.6.12" +name = "nokhwa-bindings-linux" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +checksum = "4bd666aaa41d14357817bd9a981773a73c4d00b34d344cfc244e47ebd397b1ec" dependencies = [ - "cc", - "objc2", - "objc2-foundation", - "time", + "nokhwa-core", + "v4l", ] [[package]] -name = "mbox" -version = "0.7.1" +name = "nokhwa-bindings-macos" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de" +checksum = "de78eb4a2d47a68f490899aa0516070d7a972f853ec2bb374ab53be0bd39b60f" dependencies = [ - "libc", - "stable_deref_trait", + "block", + "cocoa-foundation", + "core-foundation 0.10.1", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", ] [[package]] -name = "memchr" -version = "2.8.0" +name = "nokhwa-bindings-windows" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "899799275c93ef69bbe8cb888cf6f8249abe751cbc50be5299105022aec14a1c" +dependencies = [ + "nokhwa-core", + "once_cell", + "windows 0.62.2", +] [[package]] -name = "memoffset" -version = "0.9.1" +name = "nokhwa-core" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "109975552bbd690894f613bce3d408222911e317197c72b2e8b9a1912dc261ae" dependencies = [ - "autocfg", + "bytes", + "image", + "mozjpeg", + "thiserror 2.0.18", ] [[package]] -name = "nix" -version = "0.30.1" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", + "memchr", + "minimal-lexical", ] [[package]] -name = "nix" -version = "0.31.2" +name = "nom" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", + "memchr", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "notify-rust" version = "4.12.0" @@ -1508,6 +2940,25 @@ dependencies = [ "zbus", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -1525,6 +2976,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1534,6 +3005,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1543,6 +3024,16 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + [[package]] name = "objc2" version = "0.6.4" @@ -1558,7 +3049,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -1575,13 +3066,22 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "libc", "objc2", "objc2-core-foundation", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "oid" version = "0.2.1" @@ -1609,6 +3109,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if 1.0.4", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -1625,6 +3169,30 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "ort" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" +dependencies = [ + "ndarray", + "ort-sys", + "smallvec", + "tracing", + "ureq", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +dependencies = [ + "hmac-sha256", + "lzma-rust2", + "ureq", +] + [[package]] name = "p256" version = "0.13.2" @@ -1666,7 +3234,7 @@ dependencies = [ "passless-config-doc", "serde", "soft-fido2", - "thiserror", + "thiserror 2.0.18", "toml", ] @@ -1681,17 +3249,22 @@ dependencies = [ "clap", "clap_complete", "ctrlc", + "dirs", "env_logger", + "face_id", + "futures-util", "git2", "hex", "hmac", + "image", "libc", "log", "nix 0.31.2", + "nokhwa", "notify-rust", "passless-core", "prs-lib", - "rand", + "rand 0.8.5", "rpassword", "serde", "serde_bytes", @@ -1703,10 +3276,31 @@ dependencies = [ "soft-fido2-ctap", "soft-fido2-transport", "tempfile", + "tokio", "tss-esapi", + "which 7.0.3", + "zbus", "zeroize", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1763,6 +3357,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.5" @@ -1790,17 +3390,30 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -1810,7 +3423,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures", "opaque-debug", "universal-hash", @@ -1855,6 +3468,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1888,9 +3511,9 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags", + "bitflags 2.11.0", "procfs-core", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -1899,10 +3522,29 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hex", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "prs-lib" version = "0.5.7" @@ -1918,13 +3560,34 @@ dependencies = [ "secstr", "shellexpand", "shlex", - "thiserror", + "thiserror 2.0.18", "version-compare", "walkdir", - "which", + "which 8.0.2", "zeroize", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -1953,30 +3616,135 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if 1.0.4", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ - "libc", - "rand_chacha", - "rand_core", + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rawpointer" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ - "ppv-lite86", - "rand_core", + "either", + "rayon-core", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "rayon-core" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "getrandom 0.2.17", + "crossbeam-deque", + "crossbeam-utils", ] [[package]] @@ -1987,7 +3755,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -2019,6 +3787,49 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -2029,6 +3840,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.4", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rpassword" version = "7.4.0" @@ -2050,6 +3884,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2059,17 +3899,65 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2078,6 +3966,21 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2087,6 +3990,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2116,6 +4028,29 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -2205,13 +4140,25 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -2261,7 +4208,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", ] [[package]] @@ -2279,6 +4254,27 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "soft-fido2" version = "0.12.1" @@ -2292,7 +4288,7 @@ dependencies = [ "hkdf", "hmac", "p256", - "rand", + "rand 0.8.5", "secstr", "serde", "serde_bytes", @@ -2301,7 +4297,7 @@ dependencies = [ "soft-fido2-crypto", "soft-fido2-ctap", "soft-fido2-transport", - "spin", + "spin 0.10.0", "zeroize", ] @@ -2317,10 +4313,10 @@ dependencies = [ "hkdf", "hmac", "p256", - "rand", + "rand 0.8.5", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", "zeroize", ] @@ -2332,7 +4328,7 @@ checksum = "d199ddb1bd529743903b2009babb936551ee2d73bc4b86003bb2751d0498096d" dependencies = [ "cbor4ii", "core2", - "rand", + "rand 0.8.5", "secstr", "serde", "serde_bytes", @@ -2340,7 +4336,7 @@ dependencies = [ "smallvec", "soft-fido2-crypto", "subtle", - "thiserror", + "thiserror 2.0.18", "zeroize", ] @@ -2357,6 +4353,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spin" version = "0.10.0" @@ -2416,6 +4421,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2427,6 +4441,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -2440,8 +4475,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml", - "thiserror", - "windows", + "thiserror 2.0.18", + "windows 0.61.3", "windows-version", ] @@ -2454,8 +4489,17 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", - "windows-sys 0.52.0", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -2464,14 +4508,108 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half 2.7.1", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2479,46 +4617,36 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.47" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", + "native-tls", + "tokio", ] [[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "num-conv", - "time-core", + "rustls", + "tokio", ] [[package]] -name = "tinystr" -version = "0.8.2" +name = "tokio-util" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "displaydoc", - "zerovec", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -2572,6 +4700,51 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -2603,6 +4776,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tss-esapi" version = "7.6.0" @@ -2676,7 +4855,7 @@ checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2685,12 +4864,24 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2701,6 +4892,48 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" +dependencies = [ + "base64 0.22.1", + "cookie_store", + "der", + "flate2", + "log", + "native-tls", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -2713,6 +4946,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2736,6 +4975,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2764,6 +5034,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2785,13 +5064,27 @@ version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if 1.0.4", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -2824,6 +5117,87 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix 1.1.4", + "winsafe", +] + [[package]] name = "which" version = "8.0.2" @@ -2833,6 +5207,16 @@ dependencies = [ "libc", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2855,7 +5239,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2870,11 +5254,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -2886,6 +5282,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -2920,7 +5325,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -2967,6 +5383,27 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -3055,6 +5492,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -3121,6 +5567,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3133,6 +5585,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.1" @@ -3178,7 +5636,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix", + "rustix 1.1.4", "serde", "serde_repr", "tracing", @@ -3317,6 +5775,30 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.10.0" diff --git a/Cargo.toml b/Cargo.toml index c31e7d1a..d95583bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,14 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } hex = "0.4" shadow-rs = "1.4" +# UV provider dependencies +zbus = "5" +nokhwa = { version = "0.10", features = ["input-native"] } +face_id = "0.4" +image = "0.25" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } +which = "7" + # Proc macro dependencies syn = { version = "2.0", features = ["full", "extra-traits"] } quote = "1.0" diff --git a/cmd/passless/Cargo.toml b/cmd/passless/Cargo.toml index 239b6428..7d6bdb19 100644 --- a/cmd/passless/Cargo.toml +++ b/cmd/passless/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [features] default = [] tpm = ["passless-core/tpm", "tss-esapi"] +face = ["dep:nokhwa", "dep:face_id", "dep:image"] [dependencies] passless-core.workspace = true @@ -40,12 +41,24 @@ clap.workspace = true zeroize.workspace = true hex.workspace = true shadow-rs.workspace = true +dirs.workspace = true hmac = "0.12" rpassword = "7.3" atty = "0.2" serde_bytes = "0.11.19" tss-esapi = { version = "7.6.0", optional = true } +# UV provider dependencies +zbus.workspace = true +tokio.workspace = true +which.workspace = true +futures-util = "0.3" + +# Face recognition (optional, heavy dependencies) +nokhwa = { workspace = true, optional = true } +face_id = { workspace = true, optional = true } +image = { workspace = true, optional = true } + [dev-dependencies] base64 = "0.22" ciborium = "0.2" diff --git a/cmd/passless/src/authenticator.rs b/cmd/passless/src/authenticator.rs index b8b443ce..ef0679ab 100644 --- a/cmd/passless/src/authenticator.rs +++ b/cmd/passless/src/authenticator.rs @@ -1,9 +1,15 @@ -use crate::notification::show_verification_notification; use crate::pin_storage::PinStorage; use crate::storage::{CredentialFilter, CredentialStorage}; use crate::util::bytes_to_hex; +use crate::uv::{ + FprintdProvider, NotificationProvider, UserVerificationManager, UserVerificationProvider, + VerificationContext, VerificationResult, +}; + +#[cfg(feature = "face")] +use crate::uv::FaceIdProvider; -use passless_core::config::{PinConfig, PinEnforcement, SecurityConfig}; +use passless_core::config::{PinConfig, PinEnforcement, SecurityConfig, UvConfig, UvProviderType}; use soft_fido2::{ Authenticator, AuthenticatorCallbacks, AuthenticatorConfig, AuthenticatorOptions, Credential, @@ -23,7 +29,6 @@ static VERSION: LazyLock = LazyLock::new(|| { (major << 16) | (minor << 8) | patch }); -/// Wrapper to adapt passless PinStorage to soft-fido2 PinStorageCallbacks struct PinStorageWrapper(Arc>); impl soft_fido2::PinStorageCallbacks for PinStorageWrapper

{ @@ -38,12 +43,12 @@ impl soft_fido2::PinStorageCallbacks for PinStorageWrap } } -/// Passless authenticator callbacks implementation pub struct PasslessCallbacks { storage: Arc>, pin_storage: Option>>, security_config: SecurityConfig, pin_config: PinConfig, + uv_manager: UserVerificationManager, } impl PasslessCallbacks { @@ -52,29 +57,35 @@ impl PasslessCallbacks { pin_storage: Option>>, security_config: SecurityConfig, pin_config: PinConfig, + uv_config: UvConfig, ) -> Self { + let uv_manager = build_uv_manager(&uv_config); + Self { storage, pin_storage, security_config, pin_config, + uv_manager, } } -} -impl AuthenticatorCallbacks for PasslessCallbacks { - fn request_up(&self, info: &str, user: Option<&str>, rp: &str) -> Result { - // Check for E2E test mode (only available in debug builds) + fn do_user_verification( + &self, + operation: &str, + user: Option<&str>, + rp: &str, + ) -> Result { #[cfg(debug_assertions)] { if std::env::var("PASSLESS_E2E_AUTO_ACCEPT_UV").is_ok() { info!("E2E test mode: Auto-accepting user verification"); - return Ok(UpResult::Accepted); + return Ok(VerificationResult::Accepted); } } - let is_registration = info.to_lowercase().contains("registration") - && !info.to_lowercase().contains("credential excluded"); + let is_registration = operation.to_lowercase().contains("registration") + && !operation.to_lowercase().contains("credential excluded"); let should_verify = if is_registration { self.security_config.user_verification_registration @@ -82,17 +93,22 @@ impl AuthenticatorCallbacks for PasslessCal self.security_config.user_verification_authentication }; - let storage = match self.storage.lock() { - Ok(s) => s, - Err(_) => { - error!("Failed to acquire storage lock during user verification request"); - return Err(soft_fido2::Error::Other); - } - }; + { + let storage = match self.storage.lock() { + Ok(s) => s, + Err(_) => { + error!("Failed to acquire storage lock during user verification request"); + return Err(soft_fido2::Error::Other); + } + }; - if storage.disable_user_verification() && !is_registration && !should_verify { - debug!("User verification handled by backend (e.g., GPG): {}", info); - return Ok(UpResult::Accepted); + if storage.disable_user_verification() && !is_registration && !should_verify { + debug!( + "User verification handled by backend (e.g., GPG): {}", + operation + ); + return Ok(VerificationResult::Accepted); + } } if !should_verify { @@ -103,37 +119,87 @@ impl AuthenticatorCallbacks for PasslessCal } else { "authentication" }, - info + operation ); - return Ok(UpResult::Accepted); + return Ok(VerificationResult::Accepted); } - match show_verification_notification( - info, - Some(rp), - user, - self.security_config.notification_timeout, - ) { - Ok(crate::notification::NotificationResult::Accepted) => Ok(UpResult::Accepted), - Ok(crate::notification::NotificationResult::Denied) => Ok(UpResult::Denied), - Err(e) => { - error!("Failed to show notification: {}", e); - Err(soft_fido2::Error::Other) + let context = VerificationContext::new(operation) + .with_relying_party(rp) + .with_user(user.unwrap_or("")) + .with_timeout(self.security_config.notification_timeout); + + self.uv_manager.verify(&context).map_err(|e| { + warn!("User verification failed: {}", e); + soft_fido2::Error::Other + }) + } +} + +fn build_uv_manager(config: &UvConfig) -> UserVerificationManager { + let mut providers: Vec> = Vec::new(); + + match config.provider { + UvProviderType::Auto => { + if config.fprintd.enabled { + providers.push(Box::new(FprintdProvider::new())); + } + #[cfg(feature = "face")] + if config.face.enabled { + providers.push(Box::new(FaceIdProvider::new( + config.face.camera_index, + config.face.threshold, + None, + ))); + } + providers.push(Box::new(NotificationProvider::new(config.timeout_seconds))); + } + UvProviderType::Fprintd => { + if config.fprintd.enabled { + providers.push(Box::new(FprintdProvider::new())); + } + } + UvProviderType::Face => { + #[cfg(feature = "face")] + if config.face.enabled { + providers.push(Box::new(FaceIdProvider::new( + config.face.camera_index, + config.face.threshold, + None, + ))); + } + #[cfg(not(feature = "face"))] + { + log::warn!("Face provider requested but 'face' feature not enabled"); + } + } + UvProviderType::Notification => { + providers.push(Box::new(NotificationProvider::new(config.timeout_seconds))); + } + UvProviderType::Command => { + if config.command.enabled && !config.command.command.is_empty() { + use crate::uv::CommandProvider; + providers.push(Box::new(CommandProvider::new( + config.command.command.clone(), + config.timeout_seconds, + ))); } } } - fn request_uv(&self, info: &str, user: Option<&str>, rp: &str) -> Result { - // Check for E2E test mode (only available in debug builds) - #[cfg(debug_assertions)] - { - if std::env::var("PASSLESS_E2E_AUTO_ACCEPT_UV").is_ok() { - info!("E2E test mode: Auto-accepting user verification"); - return Ok(UvResult::Accepted); - } + UserVerificationManager::new(providers) +} + +impl AuthenticatorCallbacks for PasslessCallbacks { + fn request_up(&self, info: &str, user: Option<&str>, rp: &str) -> Result { + match self.do_user_verification(info, user, rp)? { + VerificationResult::Accepted => Ok(UpResult::Accepted), + VerificationResult::Denied => Ok(UpResult::Denied), + VerificationResult::Timeout => Ok(UpResult::Denied), } + } - // Check if PIN is set and apply enforcement policy + fn request_uv(&self, info: &str, user: Option<&str>, rp: &str) -> Result { if let Some(pin_storage) = &self.pin_storage && let storage = pin_storage.lock().map_err(|_| soft_fido2::Error::Other)? && let Ok(state) = storage.load_pin_state() @@ -152,33 +218,29 @@ impl AuthenticatorCallbacks for PasslessCal return Ok(UvResult::Denied); } info!( - "PIN is set, always_uv=false, enforcement=optional, using notification fallback" + "PIN is set, always_uv=false, enforcement=optional, using biometric/notification fallback" ); } PinEnforcement::Never => { - info!("PIN is set but enforcement=never, using notification fallback"); + info!( + "PIN is set but enforcement=never, using biometric/notification fallback" + ); } } } - // No PIN set or enforcement allows notification fallback - match show_verification_notification( - info, - Some(rp), - user, - self.security_config.notification_timeout, - ) { - Ok(crate::notification::NotificationResult::Accepted) => { - info!("User verification via notification: accepted"); + match self.do_user_verification(info, user, rp)? { + VerificationResult::Accepted => { + info!("User verification accepted"); Ok(UvResult::Accepted) } - Ok(crate::notification::NotificationResult::Denied) => { - warn!("User verification via notification: denied"); + VerificationResult::Denied => { + warn!("User verification denied"); Ok(UvResult::Denied) } - Err(e) => { - error!("Failed to show notification: {}", e); - Err(soft_fido2::Error::Other) + VerificationResult::Timeout => { + warn!("User verification timed out"); + Ok(UvResult::Denied) } } } @@ -376,54 +438,48 @@ impl soft_fido2::PinStorageCallbacks } } -/// Main authenticator service -/// -/// This service orchestrates the FIDO2 authenticator: -/// - Storage is injected through the CredentialStorage trait -/// - Handles CTAP requests and generates responses pub struct AuthenticatorService { - /// The underlying soft_fido2 authenticator pub authenticator: Authenticator>, - /// Storage backend (injected dependency) pub storage: Arc>, } impl AuthenticatorService { - /// Create a new authenticator service without PIN storage #[allow(dead_code)] - pub fn new(storage: S, security_config: SecurityConfig, pin_config: PinConfig) -> Result { - Self::with_pin_storage(storage, None, security_config, pin_config) + pub fn new( + storage: S, + security_config: SecurityConfig, + pin_config: PinConfig, + uv_config: UvConfig, + ) -> Result { + Self::with_pin_storage(storage, None, security_config, pin_config, uv_config) } } impl AuthenticatorService { - /// Create a new authenticator service with optional PIN storage pub fn with_pin_storage( storage: S, pin_storage: Option>>, security_config: SecurityConfig, pin_config: PinConfig, + uv_config: UvConfig, ) -> Result { - // Hardcoded authenticator options (FIDO2 spec compliant) - // These are platform authenticator defaults and shouldn't need configuration let options = AuthenticatorOptions { - rk: true, // Resident keys (passkeys) - up: true, // User presence - uv: Some(true), // Built-in UV capability (notification-based) - plat: true, // Platform authenticator - client_pin: Some(true), // Client PIN capability - pin_uv_auth_token: Some(true), // PIN/UV auth token support - cred_mgmt: Some(true), // Credential management - bio_enroll: None, // No biometric enrollment - large_blobs: None, // No large blob storage - ep: None, // Enterprise attestation not enabled + rk: true, + up: true, + uv: Some(true), + plat: true, + client_pin: Some(true), + pin_uv_auth_token: Some(true), + cred_mgmt: Some(true), + bio_enroll: None, + large_blobs: None, + ep: None, always_uv: Some(security_config.always_uv), make_cred_uv_not_required: Some(true), }; let config = AuthenticatorConfig::builder() .aaguid([ - // "fido.passless.rs" 0x66, 0x69, 0x64, 0x6F, 0x2E, 0x70, 0x61, 0x73, 0x73, 0x6C, 0x65, 0x73, 0x73, 0x2E, 0x72, 0x73, ]) @@ -451,6 +507,7 @@ impl AuthenticatorServi pin_storage.clone(), security_config, pin_config, + uv_config, ); let authenticator = if let Some(ps) = pin_storage { @@ -465,28 +522,16 @@ impl AuthenticatorServi }) } - /// Process a CTAP request and generate a response - /// Ok(()) on success or an error pub fn handle(&mut self, request: &[u8], response_buffer: &mut Vec) -> Result<()> { self.authenticator.handle(request, response_buffer)?; Ok(()) } - /// Get storage information pub fn storage_info(&self) -> String { let storage = self.storage.lock().unwrap(); format!("Credentials in storage: {}", storage.count_credentials()) } - /// Register a custom CTAP command handler - /// - /// This allows registering vendor-specific commands (0x40-0xFF range). - /// Useful for compatibility with different authenticator variants. - /// - /// # Arguments - /// - /// * `command` - Command byte (0x40-0xFF vendor range) - /// * `handler` - Handler function that processes the command pub fn register_custom_command(&mut self, command: u8, handler: F) where F: Fn(&[u8]) -> core::result::Result, soft_fido2::StatusCode> @@ -526,11 +571,81 @@ mod tests { }; let pin_config = PinConfig::default(); + let uv_config = UvConfig::default(); - let service = AuthenticatorService::new(storage, security_config, pin_config); + let service = AuthenticatorService::new(storage, security_config, pin_config, uv_config); assert!(service.is_ok(), "Service creation should succeed"); - // Cleanup + let _ = std::fs::remove_dir_all(temp_dir); + } + + #[test] + fn test_service_with_custom_uv_config() { + let temp_dir = std::env::temp_dir().join("test_passless_uv"); + if let Err(e) = std::fs::create_dir_all(&temp_dir) { + panic!("Failed to create temp directory: {}", e); + } + let storage = match LocalStorageAdapter::new(temp_dir.clone()) { + Ok(s) => s, + Err(e) => panic!("Failed to create local storage: {}", e), + }; + + let security_config = SecurityConfig { + check_mlock: false, + disable_core_dumps: false, + constant_signature_counter: false, + always_uv: false, + user_verification_registration: false, + user_verification_authentication: false, + notification_timeout: 30, + }; + + let pin_config = PinConfig::default(); + let uv_config = UvConfig { + provider: passless_core::config::UvProviderType::Notification, + timeout_seconds: 60, + ..Default::default() + }; + + let service = AuthenticatorService::new(storage, security_config, pin_config, uv_config); + assert!( + service.is_ok(), + "Service creation with custom UV config should succeed" + ); + + let _ = std::fs::remove_dir_all(temp_dir); + } + + #[test] + fn test_service_with_uv_disabled() { + let temp_dir = std::env::temp_dir().join("test_passless_no_uv"); + if let Err(e) = std::fs::create_dir_all(&temp_dir) { + panic!("Failed to create temp directory: {}", e); + } + let storage = match LocalStorageAdapter::new(temp_dir.clone()) { + Ok(s) => s, + Err(e) => panic!("Failed to create local storage: {}", e), + }; + + let security_config = SecurityConfig { + check_mlock: false, + disable_core_dumps: false, + constant_signature_counter: false, + always_uv: false, + user_verification_registration: false, + user_verification_authentication: false, + notification_timeout: 30, + }; + + let pin_config = PinConfig::default(); + let uv_config = UvConfig::default(); + + let service = AuthenticatorService::new(storage, security_config, pin_config, uv_config); + assert!( + service.is_ok(), + "Service creation with UV disabled should succeed" + ); + let _ = std::fs::remove_dir_all(temp_dir); } } diff --git a/cmd/passless/src/commands/custom.rs b/cmd/passless/src/commands/custom.rs index bfe59048..a33e9099 100644 --- a/cmd/passless/src/commands/custom.rs +++ b/cmd/passless/src/commands/custom.rs @@ -1,7 +1,7 @@ /// Custom credential management command handler /// /// This module provides compatibility with the Yubikey credential management variant (0x41). -/// The standard credential management (0x0a) is handled by soft-fido2's built-in implementation, +/// The standard credential management (0x0a) is handled by soft_fido2's built-in implementation, /// which properly calls the AuthenticatorCallbacks methods (enumerate_rps, list_credentials, etc.) /// that read from the actual storage backend. use crate::authenticator::AuthenticatorService; @@ -50,7 +50,7 @@ mod tests { #[test] fn test_register_yubikey_command() { - use passless_core::config::{PinConfig, SecurityConfig}; + use passless_core::config::{PinConfig, SecurityConfig, UvConfig}; let temp_dir = std::env::temp_dir().join("test_passless_custom"); if let Err(e) = std::fs::create_dir_all(&temp_dir) { @@ -61,8 +61,12 @@ mod tests { Err(e) => panic!("Failed to create local storage: {}", e), }; - let service = - AuthenticatorService::new(storage, SecurityConfig::default(), PinConfig::default()); + let service = AuthenticatorService::new( + storage, + SecurityConfig::default(), + PinConfig::default(), + UvConfig::default(), + ); assert!(service.is_ok(), "Service creation should succeed"); register_yubikey_credential_mgmt(&mut service.unwrap()); diff --git a/cmd/passless/src/main.rs b/cmd/passless/src/main.rs index 4eabe008..388cc32c 100644 --- a/cmd/passless/src/main.rs +++ b/cmd/passless/src/main.rs @@ -4,6 +4,7 @@ mod notification; mod pin_storage; mod storage; mod util; +mod uv; use passless_core::{ AppConfig, Args, BackendConfig, ClientAction, Commands, ConfigAction, Error, PinAction, Result, @@ -298,11 +299,9 @@ fn run() -> Result<()> { info!("Creating authenticator service..."); - // Get security config let security_config = config.security_config(); - - // Get PIN config let pin_config = config.pin_config(); + let uv_config = config.uv_config(); match config.backend().map_err(|e| { error!("Failed to load backend config: {}", e); @@ -317,6 +316,7 @@ fn run() -> Result<()> { Some(pin_storage), security_config, pin_config, + uv_config, )?; run_with_service(service, uhid, shutdown) } @@ -338,6 +338,7 @@ fn run() -> Result<()> { Some(pin_storage), security_config, pin_config, + uv_config, )?; run_with_service(service, uhid, shutdown) } @@ -351,6 +352,7 @@ fn run() -> Result<()> { Some(pin_storage), security_config, pin_config, + uv_config, )?; run_with_service(service, uhid, shutdown) } diff --git a/cmd/passless/src/notification.rs b/cmd/passless/src/notification.rs index 08869c5f..268bb201 100644 --- a/cmd/passless/src/notification.rs +++ b/cmd/passless/src/notification.rs @@ -48,6 +48,7 @@ fn requires_default_action() -> bool { } /// Show a user verification notification and wait for response +#[allow(dead_code)] pub fn show_verification_notification( operation: &str, relying_party: Option<&str>, diff --git a/cmd/passless/src/uv/command.rs b/cmd/passless/src/uv/command.rs new file mode 100644 index 00000000..408255ff --- /dev/null +++ b/cmd/passless/src/uv/command.rs @@ -0,0 +1,258 @@ +//! Command-based User Verification Provider +//! +//! Executes a user-defined command/script for verification. +//! +//! # Configuration +//! +//! The command should: +//! - Exit with code 0 for accepted +//! - Exit with non-zero code for denied +//! - Receive operation context via environment variables +//! +//! # Environment Variables +//! +//! - `PASSLESS_UV_OPERATION`: Operation type (registration/authentication) +//! - `PASSLESS_UV_RELYING_PARTY`: Relying party ID (if available) +//! - `PASSLESS_UV_USER`: User identifier (if available) +//! - `PASSLESS_UV_TIMEOUT`: Timeout in seconds + +use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; + +use log::{debug, info}; + +use std::process::Command; + +/// Command-based user verification provider +pub struct CommandProvider { + command: Vec, + #[allow(dead_code)] + timeout_seconds: u32, +} + +impl CommandProvider { + /// Create a new command provider + /// + /// # Arguments + /// + /// * `command` - Command and arguments to execute + /// * `timeout_seconds` - Timeout for the command + pub fn new(command: Vec, timeout_seconds: u32) -> Self { + Self { + command, + timeout_seconds, + } + } + + /// Execute the verification command + fn execute( + &self, + context: &VerificationContext, + ) -> Result { + if self.command.is_empty() { + return Err(VerificationError::NotAvailable( + "No command configured".into(), + )); + } + + let program = &self.command[0]; + let args = &self.command[1..]; + + debug!("Executing UV command: {} {:?}", program, args); + + let mut cmd = Command::new(program); + cmd.args(args); + + cmd.env("PASSLESS_UV_OPERATION", &context.operation); + + if let Some(ref rp) = context.relying_party { + cmd.env("PASSLESS_UV_RELYING_PARTY", rp); + } + + if let Some(ref user) = context.user { + cmd.env("PASSLESS_UV_USER", user); + } + + cmd.env("PASSLESS_UV_TIMEOUT", context.timeout_seconds.to_string()); + + let result = cmd.output().map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + VerificationError::NotAvailable(format!("Command not found: {}", program)) + } else { + VerificationError::DeviceError(format!("Failed to execute command: {}", e)) + } + })?; + + debug!( + "Command exit code: {}, stdout: {}, stderr: {}", + result.status.code().unwrap_or(-1), + String::from_utf8_lossy(&result.stdout).trim(), + String::from_utf8_lossy(&result.stderr).trim() + ); + + if result.status.success() { + info!("User verification accepted via command"); + Ok(VerificationResult::Accepted) + } else { + info!( + "User verification denied via command (exit code: {:?})", + result.status.code() + ); + Ok(VerificationResult::Denied) + } + } +} + +impl UserVerificationProvider for CommandProvider { + fn name(&self) -> &str { + "command" + } + + fn available(&self) -> bool { + if self.command.is_empty() { + return false; + } + + let program = &self.command[0]; + + if program.contains('/') { + std::path::Path::new(program).exists() + } else { + which::which(program).is_ok() + } + } + + fn verify( + &self, + context: &VerificationContext, + ) -> Result { + self.execute(context) + } + + fn priority(&self) -> u8 { + 50 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_command_provider_name() { + let provider = CommandProvider::new(vec!["echo".to_string()], 30); + assert_eq!(provider.name(), "command"); + } + + #[test] + fn test_command_provider_priority() { + let provider = CommandProvider::new(vec!["echo".to_string()], 30); + assert_eq!(provider.priority(), 50); + } + + #[test] + fn test_command_provider_empty_unavailable() { + let provider = CommandProvider::new(vec![], 30); + assert!(!provider.available()); + } + + #[test] + fn test_command_provider_empty_verify_fails() { + let provider = CommandProvider::new(vec![], 30); + let ctx = VerificationContext::new("test"); + let result = provider.verify(&ctx); + assert!(result.is_err()); + match result.unwrap_err() { + VerificationError::NotAvailable(_) => {} + _ => panic!("Expected NotAvailable error"), + } + } + + #[test] + fn test_command_provider_echo() { + let provider = CommandProvider::new(vec!["echo".to_string(), "accepted".to_string()], 30); + assert!(provider.available()); + + let ctx = VerificationContext::new("test"); + let result = provider.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Accepted); + } + + #[test] + fn test_command_provider_false() { + let provider = CommandProvider::new(vec!["false".to_string()], 30); + + if !provider.available() { + return; + } + + let ctx = VerificationContext::new("test"); + let result = provider.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Denied); + } + + #[test] + fn test_command_provider_exit_1_denied() { + let provider = CommandProvider::new( + vec!["sh".to_string(), "-c".to_string(), "exit 1".to_string()], + 30, + ); + + if !provider.available() { + return; + } + + let ctx = VerificationContext::new("test"); + let result = provider.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Denied); + } + + #[test] + fn test_command_provider_nonexistent() { + let provider = CommandProvider::new(vec!["nonexistent_command_12345".to_string()], 30); + assert!(!provider.available()); + + let ctx = VerificationContext::new("test"); + let result = provider.verify(&ctx); + assert!(result.is_err()); + match result.unwrap_err() { + VerificationError::NotAvailable(_) => {} + _ => panic!("Expected NotAvailable error"), + } + } + + #[test] + fn test_command_provider_with_context() { + let provider = CommandProvider::new( + vec![ + "sh".to_string(), + "-c".to_string(), + "test \"$PASSLESS_UV_OPERATION\" = 'registration' && test -n \"$PASSLESS_UV_RELYING_PARTY\"".to_string(), + ], + 30, + ); + + if !provider.available() { + return; + } + + let ctx = VerificationContext::new("registration") + .with_relying_party("example.com") + .with_user("alice") + .with_timeout(60); + + let result = provider.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Accepted); + } + + #[test] + fn test_command_provider_absolute_path() { + let provider = CommandProvider::new(vec!["/bin/true".to_string()], 30); + + if std::path::Path::new("/bin/true").exists() { + assert!(provider.available()); + let ctx = VerificationContext::new("test"); + let result = provider.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Accepted); + } + } +} diff --git a/cmd/passless/src/uv/face.rs b/cmd/passless/src/uv/face.rs new file mode 100644 index 00000000..7116f386 --- /dev/null +++ b/cmd/passless/src/uv/face.rs @@ -0,0 +1,447 @@ +//! Face Recognition-based User Verification Provider +//! +//! Uses webcam and face recognition for user verification. +//! +//! # Requirements +//! +//! - Webcam device (V4L2 on Linux) +//! - face_id crate for detection and recognition +//! - ONNX Runtime (downloaded automatically) +//! +//! # Enrollment +//! +//! During enrollment, multiple face embeddings are captured and averaged +//! to create a robust template. The template is stored securely. + +use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; + +use log::{debug, info, warn}; + +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use image::DynamicImage; +use nokhwa::Camera; +use nokhwa::pixel_format::RgbFormat; +use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; + +#[allow(dead_code)] +const DEFAULT_THRESHOLD: f32 = 0.6; +#[allow(dead_code)] +const ENROLLMENT_SAMPLES: usize = 5; + +/// Face recognition provider using webcam +pub struct FaceIdProvider { + camera_index: usize, + threshold: f32, + embeddings_path: PathBuf, + enrolled: Arc>>, + analyzer: Arc>>, +} + +impl FaceIdProvider { + /// Create a new face recognition provider + pub fn new(camera_index: usize, threshold: f32, embeddings_path: Option) -> Self { + let embeddings_path = embeddings_path.unwrap_or_else(|| { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("passless") + .join("face_embeddings") + }); + + Self { + camera_index, + threshold, + embeddings_path, + enrolled: Arc::new(Mutex::new(None)), + analyzer: Arc::new(Mutex::new(None)), + } + } + + /// Get path to the stored embeddings file + fn embeddings_file(&self) -> PathBuf { + self.embeddings_path.join("face_template.bin") + } + + /// Check if a camera is available + fn check_camera_available(&self) -> bool { + let index = CameraIndex::Index(self.camera_index as u32); + let format = + RequestedFormat::new::(RequestedFormatType::AbsoluteHighestFrameRate); + + Camera::new(index, format).is_ok() + } + + /// Initialize the face analyzer (downloads models if needed) + fn init_analyzer(&self) -> Result<(), VerificationError> { + let mut analyzer_guard = self.analyzer.lock().unwrap(); + + if analyzer_guard.is_some() { + return Ok(()); + } + + debug!("Initializing face analyzer (downloading models if needed)..."); + + let rt = tokio::runtime::Runtime::new().map_err(|e| { + VerificationError::DeviceError(format!("Failed to create runtime: {}", e)) + })?; + + let analyzer = rt.block_on(async { + face_id::analyzer::FaceAnalyzer::from_hf() + .build() + .await + .map_err(|e| { + VerificationError::DeviceError(format!( + "Failed to initialize face analyzer: {}", + e + )) + }) + })?; + + *analyzer_guard = Some(analyzer); + info!("Face analyzer initialized successfully"); + Ok(()) + } + + /// Capture a single frame from the camera + fn capture_frame(&self) -> Result { + debug!("Capturing frame from camera {}", self.camera_index); + + let index = CameraIndex::Index(self.camera_index as u32); + let format = + RequestedFormat::new::(RequestedFormatType::AbsoluteHighestFrameRate); + + let mut camera = Camera::new(index, format) + .map_err(|e| VerificationError::DeviceError(format!("Failed to open camera: {}", e)))?; + + camera.open_stream().map_err(|e| { + VerificationError::DeviceError(format!("Failed to start camera stream: {}", e)) + })?; + + let buffer = camera.frame().map_err(|e| { + VerificationError::DeviceError(format!("Failed to capture frame: {}", e)) + })?; + + let decoded = buffer.decode_image::().map_err(|e| { + VerificationError::DeviceError(format!("Failed to decode frame: {}", e)) + })?; + + camera + .stop_stream() + .map_err(|e| warn!("Failed to stop camera stream: {}", e)) + .ok(); + + Ok(DynamicImage::ImageRgb8(decoded)) + } + + /// Extract face embedding from an image + fn extract_embedding(&self, image: &DynamicImage) -> Result, VerificationError> { + self.init_analyzer()?; + + let analyzer_guard = self.analyzer.lock().unwrap(); + let analyzer = analyzer_guard.as_ref().unwrap(); + + debug!("Analyzing image for face"); + + let results = analyzer + .analyze(image) + .map_err(|e| VerificationError::DeviceError(format!("Face analysis failed: {}", e)))?; + + if results.is_empty() { + return Err(VerificationError::DeviceError("No face detected".into())); + } + + if results.len() > 1 { + warn!("Multiple faces detected, using the first one"); + } + + let face = &results[0]; + debug!( + "Extracted embedding with {} dimensions", + face.embedding.len() + ); + Ok(face.embedding.clone()) + } + + /// Load stored embeddings template + fn load_template(&self) -> Result, VerificationError> { + let path = self.embeddings_file(); + if !path.exists() { + return Err(VerificationError::NotEnrolled); + } + + let data = std::fs::read(&path).map_err(|e| { + VerificationError::DeviceError(format!("Failed to read template: {}", e)) + })?; + + let embedding_len = u64::from_le_bytes( + data.get(0..8) + .ok_or_else(|| VerificationError::DeviceError("Invalid template format".into()))? + .try_into() + .unwrap(), + ) as usize; + + let embedding: Vec = data[8..] + .chunks_exact(4) + .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap())) + .collect(); + + if embedding.len() != embedding_len { + return Err(VerificationError::DeviceError( + "Template length mismatch".into(), + )); + } + + Ok(embedding) + } + + /// Save embeddings template + #[allow(dead_code)] + fn save_template(&self, embedding: &[f32]) -> Result<(), VerificationError> { + std::fs::create_dir_all(&self.embeddings_path).map_err(|e| { + VerificationError::DeviceError(format!("Failed to create embeddings directory: {}", e)) + })?; + + let path = self.embeddings_file(); + + let mut data = Vec::with_capacity(8 + embedding.len() * 4); + data.extend_from_slice(&(embedding.len() as u64).to_le_bytes()); + for &val in embedding { + data.extend_from_slice(&val.to_le_bytes()); + } + + std::fs::write(&path, &data).map_err(|e| { + VerificationError::DeviceError(format!("Failed to write template: {}", e)) + })?; + + info!("Saved face template to {}", path.display()); + Ok(()) + } + + /// Calculate cosine similarity between two embeddings + fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { + if a.len() != b.len() { + return 0.0; + } + + let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); + let mag_a: f32 = a.iter().map(|x| x * x).sum::().sqrt(); + let mag_b: f32 = b.iter().map(|x| x * x).sum::().sqrt(); + + if mag_a == 0.0 || mag_b == 0.0 { + return 0.0; + } + + dot_product / (mag_a * mag_b) + } + + /// Perform face verification + fn do_verification(&self) -> Result { + let template = self.load_template()?; + + let frame = self.capture_frame()?; + let embedding = self.extract_embedding(&frame)?; + + let similarity = Self::cosine_similarity(&template, &embedding); + debug!("Face similarity score: {}", similarity); + + if similarity >= self.threshold { + info!("Face matched (similarity: {})", similarity); + Ok(VerificationResult::Accepted) + } else { + info!( + "Face not matched (similarity: {} < {})", + similarity, self.threshold + ); + Ok(VerificationResult::Denied) + } + } +} + +impl UserVerificationProvider for FaceIdProvider { + fn name(&self) -> &str { + "face" + } + + fn available(&self) -> bool { + self.check_camera_available() + } + + fn verify( + &self, + _context: &VerificationContext, + ) -> Result { + self.do_verification() + } + + fn priority(&self) -> u8 { + 90 + } + + fn supports_enrollment(&self) -> bool { + true + } + + fn requires_enrollment(&self) -> bool { + true + } + + fn is_enrolled(&self) -> bool { + if let Some(enrolled) = *self.enrolled.lock().unwrap() { + return enrolled; + } + + let enrolled = self.embeddings_file().exists(); + *self.enrolled.lock().unwrap() = Some(enrolled); + enrolled + } + + fn enroll(&self) -> Result<(), VerificationError> { + info!( + "Starting face enrollment (capturing {} samples)", + ENROLLMENT_SAMPLES + ); + + self.init_analyzer()?; + + let mut embeddings: Vec> = Vec::with_capacity(ENROLLMENT_SAMPLES); + + for i in 0..ENROLLMENT_SAMPLES { + debug!( + "Capturing enrollment sample {}/{}", + i + 1, + ENROLLMENT_SAMPLES + ); + + let frame = self.capture_frame()?; + let embedding = self.extract_embedding(&frame)?; + embeddings.push(embedding); + + if i < ENROLLMENT_SAMPLES - 1 { + std::thread::sleep(std::time::Duration::from_millis(500)); + } + } + + let avg_embedding: Vec = { + let len = embeddings[0].len(); + let mut sum = vec![0.0f32; len]; + + for embedding in &embeddings { + for (i, &val) in embedding.iter().enumerate() { + sum[i] += val; + } + } + + let n = embeddings.len() as f32; + sum.into_iter().map(|x| x / n).collect() + }; + + self.save_template(&avg_embedding)?; + + *self.enrolled.lock().unwrap() = Some(true); + + info!("Face enrollment completed successfully"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_face_provider_name() { + let provider = FaceIdProvider::new(0, DEFAULT_THRESHOLD, None); + assert_eq!(provider.name(), "face"); + } + + #[test] + fn test_face_provider_priority() { + let provider = FaceIdProvider::new(0, DEFAULT_THRESHOLD, None); + assert_eq!(provider.priority(), 90); + } + + #[test] + fn test_face_provider_supports_enrollment() { + let provider = FaceIdProvider::new(0, DEFAULT_THRESHOLD, None); + assert!(provider.supports_enrollment()); + } + + #[test] + fn test_face_provider_requires_enrollment() { + let provider = FaceIdProvider::new(0, DEFAULT_THRESHOLD, None); + assert!(provider.requires_enrollment()); + } + + #[test] + fn test_face_provider_not_enrolled_initially() { + let temp_dir = tempfile::tempdir().unwrap(); + let provider = FaceIdProvider::new( + 0, + DEFAULT_THRESHOLD, + Some(temp_dir.path().join("face_embeddings")), + ); + assert!(!provider.is_enrolled()); + } + + #[test] + fn test_cosine_similarity_identical() { + let a = vec![1.0, 0.0, 0.0]; + let b = vec![1.0, 0.0, 0.0]; + let sim = FaceIdProvider::cosine_similarity(&a, &b); + assert!((sim - 1.0).abs() < 0.001); + } + + #[test] + fn test_cosine_similarity_orthogonal() { + let a = vec![1.0, 0.0, 0.0]; + let b = vec![0.0, 1.0, 0.0]; + let sim = FaceIdProvider::cosine_similarity(&a, &b); + assert!(sim.abs() < 0.001); + } + + #[test] + fn test_cosine_similarity_opposite() { + let a = vec![1.0, 0.0, 0.0]; + let b = vec![-1.0, 0.0, 0.0]; + let sim = FaceIdProvider::cosine_similarity(&a, &b); + assert!((sim - (-1.0)).abs() < 0.001); + } + + #[test] + fn test_cosine_similarity_partial() { + let a = vec![1.0, 0.0, 0.0]; + let b = vec![1.0, 1.0, 0.0]; + let sim = FaceIdProvider::cosine_similarity(&a, &b); + let expected = std::f32::consts::FRAC_1_SQRT_2; + assert!((sim - expected).abs() < 0.01); + } + + #[test] + fn test_cosine_similarity_different_lengths() { + let a = vec![1.0, 0.0, 0.0]; + let b = vec![1.0, 0.0]; + let sim = FaceIdProvider::cosine_similarity(&a, &b); + assert_eq!(sim, 0.0); + } + + #[test] + fn test_cosine_similarity_zero_vectors() { + let a = vec![0.0, 0.0, 0.0]; + let b = vec![1.0, 0.0, 0.0]; + let sim = FaceIdProvider::cosine_similarity(&a, &b); + assert_eq!(sim, 0.0); + } + + #[test] + fn test_cosine_similarity_high_dim() { + let a: Vec = (0..512) + .map(|i| if i % 2 == 0 { 1.0 } else { 0.0 }) + .collect(); + let b: Vec = (0..512) + .map(|i| if i % 2 == 0 { 1.0 } else { 0.5 }) + .collect(); + let sim = FaceIdProvider::cosine_similarity(&a, &b); + assert!(sim > 0.8 && sim < 1.0); + } +} diff --git a/cmd/passless/src/uv/fprintd.rs b/cmd/passless/src/uv/fprintd.rs new file mode 100644 index 00000000..64eb4e93 --- /dev/null +++ b/cmd/passless/src/uv/fprintd.rs @@ -0,0 +1,317 @@ +//! fprintd-based User Verification Provider +//! +//! Uses the fprintd D-Bus service for fingerprint verification. +//! +//! # Requirements +//! +//! - fprintd service must be running +//! - A fingerprint reader must be connected +//! - User must have enrolled fingerprints + +use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; + +use log::{debug, error, info, warn}; + +use std::sync::{Arc, Mutex}; + +/// fprintd-based fingerprint verification provider +pub struct FprintdProvider { + available_cache: Arc>>, +} + +impl FprintdProvider { + /// Create a new fprintd provider + pub fn new() -> Self { + Self { + available_cache: Arc::new(Mutex::new(None)), + } + } + + async fn do_verify(timeout_secs: u32) -> Result { + use zbus::Connection; + + debug!("Starting fprintd verification"); + + let conn = Connection::system().await.map_err(|e| { + VerificationError::DeviceError(format!("D-Bus connection failed: {}", e)) + })?; + + let device_path: zbus::zvariant::OwnedObjectPath = conn + .call_method( + Some("net.reactivated.Fprint"), + "/net/reactivated/Fprint/Manager", + Some("net.reactivated.Fprint.Manager"), + "GetDefaultDevice", + &(), + ) + .await + .map_err(|e| VerificationError::NotAvailable(format!("No fingerprint device: {}", e)))? + .body() + .deserialize() + .map_err(|e| VerificationError::DeviceError(format!("Invalid response: {}", e)))?; + + debug!("Found device at path: {:?}", device_path); + + conn.call_method( + Some("net.reactivated.Fprint"), + device_path.as_str(), + Some("net.reactivated.Fprint.Device"), + "Claim", + &"", + ) + .await + .map_err(|e| VerificationError::DeviceError(format!("Failed to claim device: {}", e)))?; + + let result = Self::wait_for_verification(&conn, &device_path, timeout_secs).await; + + let _ = conn + .call_method( + Some("net.reactivated.Fprint"), + device_path.as_str(), + Some("net.reactivated.Fprint.Device"), + "Release", + &(), + ) + .await; + + result + } + + async fn wait_for_verification( + conn: &zbus::Connection, + device_path: &zbus::zvariant::OwnedObjectPath, + timeout_secs: u32, + ) -> Result { + use futures_util::StreamExt; + use zbus::MessageStream; + + conn.call_method( + Some("net.reactivated.Fprint"), + device_path.as_str(), + Some("net.reactivated.Fprint.Device"), + "VerifyStart", + &"any", + ) + .await + .map_err(|e| { + VerificationError::DeviceError(format!("Failed to start verification: {}", e)) + })?; + + let mut stream = MessageStream::from(conn); + + let timeout = tokio::time::sleep(std::time::Duration::from_secs(timeout_secs as u64)); + tokio::pin!(timeout); + + let mut result = VerificationResult::Timeout; + + loop { + tokio::select! { + _ = &mut timeout => { + warn!("Fingerprint verification timed out"); + break; + } + msg = stream.next() => { + match msg { + Some(Ok(msg)) => { + let header = msg.header(); + + if header.interface().map(|i| i.as_str()) != Some("net.reactivated.Fprint.Device") { + continue; + } + if header.member().map(|m| m.as_str()) != Some("VerifyStatus") { + continue; + } + if header.path().map(|p| p.as_str()) != Some(device_path.as_str()) { + continue; + } + + match msg.body().deserialize::<(&str, bool)>() { + Ok((status, _done)) => { + debug!("fprintd status: {}", status); + + match status { + "verify-match" => { + info!("Fingerprint matched"); + result = VerificationResult::Accepted; + break; + } + "verify-no-match" => { + info!("Fingerprint not matched"); + result = VerificationResult::Denied; + break; + } + "verify-retry-scan" + | "verify-swipe-too-short" + | "verify-finger-not-entered" + | "verify-remove-and-retry" => { + debug!("Retry requested: {}", status); + continue; + } + "verify-disconnected" => { + error!("Device disconnected"); + result = VerificationResult::Denied; + break; + } + "verify-unknown-error" => { + error!("Unknown error"); + result = VerificationResult::Denied; + break; + } + _ => { + warn!("Unknown status: {}", status); + continue; + } + } + } + Err(e) => { + error!("Failed to parse signal: {}", e); + continue; + } + } + } + Some(Err(e)) => { + error!("Error receiving message: {}", e); + continue; + } + None => { + error!("Signal stream closed"); + break; + } + } + } + } + } + + let _ = conn + .call_method( + Some("net.reactivated.Fprint"), + device_path.as_str(), + Some("net.reactivated.Fprint.Device"), + "VerifyStop", + &(), + ) + .await; + + Ok(result) + } + + fn check_available_sync() -> bool { + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => return false, + }; + + rt.block_on(async { + use zbus::Connection; + + match Connection::system().await { + Ok(conn) => { + match conn + .call_method( + Some("net.reactivated.Fprint"), + "/net/reactivated/Fprint/Manager", + Some("net.reactivated.Fprint.Manager"), + "GetDefaultDevice", + &(), + ) + .await + { + Ok(reply) => { + if let Ok(path) = reply + .body() + .deserialize::() + { + info!("fprintd device available at: {:?}", path); + true + } else { + false + } + } + Err(_) => false, + } + } + Err(_) => false, + } + }) + } +} + +impl Default for FprintdProvider { + fn default() -> Self { + Self::new() + } +} + +impl UserVerificationProvider for FprintdProvider { + fn name(&self) -> &str { + "fprintd" + } + + fn available(&self) -> bool { + if let Some(cached) = *self.available_cache.lock().unwrap() { + return cached; + } + + let available = Self::check_available_sync(); + *self.available_cache.lock().unwrap() = Some(available); + available + } + + fn verify( + &self, + context: &VerificationContext, + ) -> Result { + let timeout_secs = context.timeout_seconds; + + let result = std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().map_err(|e| { + VerificationError::DeviceError(format!("Failed to create runtime: {}", e)) + })?; + + rt.block_on(async { Self::do_verify(timeout_secs).await }) + }) + .join() + .map_err(|_| VerificationError::Other("Verification thread panicked".into()))??; + + Ok(result) + } + + fn priority(&self) -> u8 { + 100 + } + + fn supports_enrollment(&self) -> bool { + true + } + + fn requires_enrollment(&self) -> bool { + true + } + + fn is_enrolled(&self) -> bool { + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fprintd_provider_name() { + let provider = FprintdProvider::new(); + assert_eq!(provider.name(), "fprintd"); + } + + #[test] + fn test_fprintd_provider_priority() { + let provider = FprintdProvider::new(); + assert_eq!(provider.priority(), 100); + } + + #[test] + fn test_fprintd_provider_supports_enrollment() { + let provider = FprintdProvider::new(); + assert!(provider.supports_enrollment()); + } +} diff --git a/cmd/passless/src/uv/manager.rs b/cmd/passless/src/uv/manager.rs new file mode 100644 index 00000000..e1e3b4f8 --- /dev/null +++ b/cmd/passless/src/uv/manager.rs @@ -0,0 +1,319 @@ +//! User Verification Manager +//! +//! Manages a chain of UV providers with automatic fallback. + +use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; + +use log::{debug, info, warn}; +use std::cmp::Reverse; + +/// Manages user verification providers with fallback chain +pub struct UserVerificationManager { + providers: Vec>, +} + +impl UserVerificationManager { + /// Create a new manager with the given providers + /// + /// Providers are sorted by priority (highest first) and will be tried in order. + pub fn new(providers: Vec>) -> Self { + let mut providers = providers; + providers.sort_by_key(|p| Reverse(p.priority())); + Self { providers } + } + + /// Create a manager with default providers (empty) + #[allow(dead_code)] + pub fn empty() -> Self { + Self { + providers: Vec::new(), + } + } + + /// Add a provider to the chain + #[allow(dead_code)] + pub fn add_provider(&mut self, provider: Box) { + self.providers.push(provider); + self.providers.sort_by_key(|p| Reverse(p.priority())); + } + + /// Get list of available providers + pub fn available_providers(&self) -> Vec<&dyn UserVerificationProvider> { + self.providers + .iter() + .filter(|p| p.available()) + .map(|p| p.as_ref()) + .collect() + } + + /// Perform user verification with automatic fallback + /// + /// Tries providers in priority order until one succeeds. + /// Falls back to next provider on error or timeout. + /// Returns first non-error result (Accepted, Denied, or Timeout from last provider). + pub fn verify( + &self, + context: &VerificationContext, + ) -> Result { + let available: Vec<_> = self.available_providers(); + + if available.is_empty() { + warn!("No UV providers available"); + return Err(VerificationError::NotAvailable( + "No user verification providers available".into(), + )); + } + + info!( + "Starting user verification with {} available provider(s)", + available.len() + ); + + let mut last_error: Option = None; + + for provider in &available { + debug!( + "Trying UV provider: {} (priority {})", + provider.name(), + provider.priority() + ); + + // Check enrollment + if provider.requires_enrollment() && !provider.is_enrolled() { + debug!("Provider {} not enrolled, skipping", provider.name()); + last_error = Some(VerificationError::NotEnrolled); + continue; + } + + match provider.verify(context) { + Ok(result @ VerificationResult::Accepted) => { + info!("User verification accepted via {}", provider.name()); + return Ok(result); + } + Ok(result @ VerificationResult::Denied) => { + info!("User verification denied via {}", provider.name()); + return Ok(result); + } + Ok(VerificationResult::Timeout) => { + warn!("UV provider {} timed out, trying next", provider.name()); + last_error = Some(VerificationError::Other("Timeout".into())); + continue; + } + Err(e) => { + warn!("UV provider {} failed: {}, trying next", provider.name(), e); + last_error = Some(e); + continue; + } + } + } + + // All providers failed + let error = last_error.unwrap_or_else(|| { + VerificationError::Other("All verification providers failed".into()) + }); + Err(error) + } + + /// Get a provider by name for enrollment + #[allow(dead_code)] + pub fn get_provider(&self, name: &str) -> Option<&dyn UserVerificationProvider> { + self.providers + .iter() + .find(|p| p.name() == name) + .map(|p| p.as_ref()) + } + + /// List all providers (for CLI status command) + #[allow(dead_code)] + pub fn list_providers(&self) -> &[Box] { + &self.providers + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct MockProvider { + name: &'static str, + priority: u8, + available: bool, + result: VerificationResult, + requires_enrollment: bool, + is_enrolled: bool, + } + + impl MockProvider { + fn new( + name: &'static str, + priority: u8, + available: bool, + result: VerificationResult, + ) -> Self { + Self { + name, + priority, + available, + result, + requires_enrollment: false, + is_enrolled: true, + } + } + + fn with_enrollment(mut self, requires: bool, enrolled: bool) -> Self { + self.requires_enrollment = requires; + self.is_enrolled = enrolled; + self + } + } + + impl UserVerificationProvider for MockProvider { + fn name(&self) -> &str { + self.name + } + + fn available(&self) -> bool { + self.available + } + + fn verify( + &self, + _context: &VerificationContext, + ) -> Result { + if !self.available { + return Err(VerificationError::NotAvailable("Not available".into())); + } + Ok(self.result) + } + + fn priority(&self) -> u8 { + self.priority + } + + fn requires_enrollment(&self) -> bool { + self.requires_enrollment + } + + fn is_enrolled(&self) -> bool { + self.is_enrolled + } + } + + #[test] + fn test_provider_ordering_by_priority() { + let low = MockProvider::new("low", 10, true, VerificationResult::Accepted); + let high = MockProvider::new("high", 100, true, VerificationResult::Accepted); + + let manager = UserVerificationManager::new(vec![Box::new(low), Box::new(high)]); + + assert_eq!(manager.list_providers()[0].name(), "high"); + assert_eq!(manager.list_providers()[1].name(), "low"); + } + + #[test] + fn test_fallback_on_timeout() { + let fail = MockProvider::new("fail", 100, true, VerificationResult::Timeout); + let succeed = MockProvider::new("succeed", 50, true, VerificationResult::Accepted); + + let manager = UserVerificationManager::new(vec![Box::new(fail), Box::new(succeed)]); + + let ctx = VerificationContext::new("test"); + let result = manager.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Accepted); + } + + #[test] + fn test_no_available_providers() { + let unavailable = + MockProvider::new("unavailable", 100, false, VerificationResult::Accepted); + + let manager = UserVerificationManager::new(vec![Box::new(unavailable)]); + let ctx = VerificationContext::new("test"); + let result = manager.verify(&ctx); + + assert!(result.is_err()); + match result.unwrap_err() { + VerificationError::NotAvailable(_) => {} + _ => panic!("Expected NotAvailable error"), + } + } + + #[test] + fn test_denied_returns_immediately() { + let deny = MockProvider::new("deny", 100, true, VerificationResult::Denied); + let accept = MockProvider::new("accept", 50, true, VerificationResult::Accepted); + + let manager = UserVerificationManager::new(vec![Box::new(deny), Box::new(accept)]); + + let ctx = VerificationContext::new("test"); + let result = manager.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Denied); + } + + #[test] + fn test_skip_unenrolled_provider() { + let unenrolled = MockProvider::new("unenrolled", 100, true, VerificationResult::Accepted) + .with_enrollment(true, false); + let fallback = MockProvider::new("fallback", 50, true, VerificationResult::Accepted); + + let manager = UserVerificationManager::new(vec![Box::new(unenrolled), Box::new(fallback)]); + + let ctx = VerificationContext::new("test"); + let result = manager.verify(&ctx); + assert_eq!(result.unwrap(), VerificationResult::Accepted); + } + + #[test] + fn test_all_providers_fail() { + let fail1 = MockProvider::new("fail1", 100, true, VerificationResult::Timeout); + let fail2 = MockProvider::new("fail2", 50, true, VerificationResult::Timeout); + + let manager = UserVerificationManager::new(vec![Box::new(fail1), Box::new(fail2)]); + + let ctx = VerificationContext::new("test"); + let result = manager.verify(&ctx); + assert!(result.is_err()); + } + + #[test] + fn test_available_providers_filter() { + let available = MockProvider::new("available", 100, true, VerificationResult::Accepted); + let unavailable = MockProvider::new("unavailable", 50, false, VerificationResult::Accepted); + + let manager = + UserVerificationManager::new(vec![Box::new(available), Box::new(unavailable)]); + + let available_list = manager.available_providers(); + assert_eq!(available_list.len(), 1); + assert_eq!(available_list[0].name(), "available"); + } + + #[test] + fn test_get_provider_by_name() { + let provider = MockProvider::new("test_provider", 100, true, VerificationResult::Accepted); + + let manager = UserVerificationManager::new(vec![Box::new(provider)]); + + assert!(manager.get_provider("test_provider").is_some()); + assert!(manager.get_provider("nonexistent").is_none()); + } + + #[test] + fn test_empty_manager() { + let manager = UserVerificationManager::empty(); + assert!(manager.list_providers().is_empty()); + + let ctx = VerificationContext::new("test"); + let result = manager.verify(&ctx); + assert!(result.is_err()); + } + + #[test] + fn test_add_provider() { + let mut manager = UserVerificationManager::empty(); + let provider = MockProvider::new("test", 100, true, VerificationResult::Accepted); + + manager.add_provider(Box::new(provider)); + assert_eq!(manager.list_providers().len(), 1); + } +} diff --git a/cmd/passless/src/uv/mod.rs b/cmd/passless/src/uv/mod.rs new file mode 100644 index 00000000..f802cf62 --- /dev/null +++ b/cmd/passless/src/uv/mod.rs @@ -0,0 +1,213 @@ +//! User Verification (UV) module for FIDO2 authenticator +//! +//! This module provides a pluggable user verification system with multiple providers: +//! - FprintdProvider: Fingerprint verification via D-Bus +//! - FaceIdProvider: Face recognition via webcam (requires "face" feature) +//! - NotificationProvider: Desktop notification (fallback) +//! - CommandProvider: Custom command/script execution + +mod command; +mod fprintd; +mod manager; +mod notification; + +#[cfg(feature = "face")] +mod face; + +use std::fmt; + +pub use command::CommandProvider; +#[cfg(feature = "face")] +pub use face::FaceIdProvider; +pub use fprintd::FprintdProvider; +pub use manager::UserVerificationManager; +pub use notification::NotificationProvider; + +/// Result of a user verification attempt +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VerificationResult { + /// User successfully verified + Accepted, + /// User explicitly denied verification + Denied, + /// Verification timed out + Timeout, +} + +/// Error during user verification +#[derive(Debug, Clone)] +pub enum VerificationError { + /// Provider is not available (not installed, no device, etc.) + NotAvailable(String), + /// Device or system error during verification + DeviceError(String), + /// User cancelled the operation + #[allow(dead_code)] + UserCancelled, + /// Provider is not enrolled (no biometric data registered) + NotEnrolled, + /// Unknown error + Other(String), +} + +impl fmt::Display for VerificationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VerificationError::NotAvailable(msg) => write!(f, "Not available: {}", msg), + VerificationError::DeviceError(msg) => write!(f, "Device error: {}", msg), + VerificationError::UserCancelled => write!(f, "User cancelled"), + VerificationError::NotEnrolled => write!(f, "Not enrolled"), + VerificationError::Other(msg) => write!(f, "Error: {}", msg), + } + } +} + +impl std::error::Error for VerificationError {} + +/// Context for user verification request +#[derive(Debug, Clone)] +pub struct VerificationContext { + /// Type of operation (e.g., "registration", "authentication") + pub operation: String, + /// Relying party ID (domain) + pub relying_party: Option, + /// User identifier + pub user: Option, + /// Timeout in seconds + pub timeout_seconds: u32, +} + +impl VerificationContext { + /// Create a new verification context + pub fn new(operation: impl Into) -> Self { + Self { + operation: operation.into(), + relying_party: None, + user: None, + timeout_seconds: 30, + } + } + + /// Set the relying party + pub fn with_relying_party(mut self, rp: impl Into) -> Self { + self.relying_party = Some(rp.into()); + self + } + + /// Set the user + pub fn with_user(mut self, user: impl Into) -> Self { + self.user = Some(user.into()); + self + } + + /// Set the timeout + pub fn with_timeout(mut self, seconds: u32) -> Self { + self.timeout_seconds = seconds; + self + } +} + +/// Trait for user verification providers +/// +/// Each provider implements biometric or other verification methods. +/// Providers are tried in order of priority until one succeeds. +pub trait UserVerificationProvider: Send + Sync { + /// Unique name for this provider + fn name(&self) -> &str; + + /// Check if this provider is available (device present, service running, etc.) + fn available(&self) -> bool; + + /// Perform user verification + /// + /// Returns: + /// - `Ok(Accepted)` - User successfully verified + /// - `Ok(Denied)` - User explicitly denied + /// - `Ok(Timeout)` - Verification timed out + /// - `Err(_)` - Error occurred, try next provider + fn verify( + &self, + context: &VerificationContext, + ) -> Result; + + /// Priority of this provider (higher = preferred) + /// + /// Default priorities: + /// - fprintd: 100 + /// - face: 90 + /// - notification: 10 + fn priority(&self) -> u8 { + 50 + } + + /// Whether this provider supports enrollment + fn supports_enrollment(&self) -> bool { + false + } + + /// Enroll user for verification (e.g., capture face, fingerprint) + #[allow(dead_code)] + fn enroll(&self) -> Result<(), VerificationError> { + Err(VerificationError::NotAvailable( + "Enrollment not supported".into(), + )) + } + + /// Whether this provider requires enrollment before use + fn requires_enrollment(&self) -> bool { + self.supports_enrollment() + } + + /// Check if the user is enrolled + fn is_enrolled(&self) -> bool { + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_context_new() { + let ctx = VerificationContext::new("registration"); + assert_eq!(ctx.operation, "registration"); + assert_eq!(ctx.relying_party, None); + assert_eq!(ctx.user, None); + assert_eq!(ctx.timeout_seconds, 30); + } + + #[test] + fn test_context_builder() { + let ctx = VerificationContext::new("authentication") + .with_relying_party("example.com") + .with_user("alice") + .with_timeout(60); + + assert_eq!(ctx.operation, "authentication"); + assert_eq!(ctx.relying_party, Some("example.com".to_string())); + assert_eq!(ctx.user, Some("alice".to_string())); + assert_eq!(ctx.timeout_seconds, 60); + } + + #[test] + fn test_verification_error_display() { + assert_eq!( + VerificationError::NotAvailable("test".into()).to_string(), + "Not available: test" + ); + assert_eq!( + VerificationError::DeviceError("failed".into()).to_string(), + "Device error: failed" + ); + assert_eq!( + VerificationError::UserCancelled.to_string(), + "User cancelled" + ); + assert_eq!(VerificationError::NotEnrolled.to_string(), "Not enrolled"); + assert_eq!( + VerificationError::Other("oops".into()).to_string(), + "Error: oops" + ); + } +} diff --git a/cmd/passless/src/uv/notification.rs b/cmd/passless/src/uv/notification.rs new file mode 100644 index 00000000..c4628212 --- /dev/null +++ b/cmd/passless/src/uv/notification.rs @@ -0,0 +1,164 @@ +//! Notification-based User Verification Provider +//! +//! Uses desktop notifications for user verification. +//! This is the fallback provider when no biometric methods are available. + +use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; + +use log::debug; + +use std::sync::{Arc, Mutex}; + +use notify_rust::{Notification, Timeout}; + +/// Check if the notification server requires special handling +fn requires_default_action() -> bool { + notify_rust::get_server_information() + .map(|info| { + let server_name = info.name.to_lowercase(); + debug!( + "Notification server: {} (version: {})", + info.name, info.version + ); + + match (server_name.as_str(), info.version.as_str()) { + ("notify-osd", "1.0") | ("mako", "0.0.0") => { + debug!("Detected {} - using default action mode", server_name); + true + } + _ => false, + } + }) + .unwrap_or_else(|e| { + debug!("Failed to get notification server info: {}", e); + false + }) +} + +/// Notification-based user verification provider +pub struct NotificationProvider { + #[allow(dead_code)] + timeout_seconds: u32, +} + +impl NotificationProvider { + /// Create a new notification provider + pub fn new(timeout_seconds: u32) -> Self { + Self { timeout_seconds } + } + + /// Show verification notification and wait for response + fn show_notification( + &self, + context: &VerificationContext, + ) -> Result { + let mut message = format!("Operation: {}", context.operation); + if let Some(rp) = &context.relying_party { + message.push_str(&format!("\nRelying Party: {}", rp)); + } + if let Some(user) = &context.user { + message.push_str(&format!("\nUser: {}", user)); + } + + debug!("Showing user verification notification"); + + let default_means_accept = requires_default_action(); + let action_result = Arc::new(Mutex::new(None)); + let action_result_clone = action_result.clone(); + + let mut notification = Notification::new(); + notification + .summary("User Verification Required") + .body(&message) + .icon("security-high") + .timeout(Timeout::Milliseconds(context.timeout_seconds * 1000)); + + if default_means_accept { + notification.action("default", ""); + } else { + notification.action("approve", "Accept"); + notification.action("deny", "Deny"); + } + + let handle = notification.show().map_err(|e| { + VerificationError::DeviceError(format!("Failed to show notification: {}", e)) + })?; + + handle.wait_for_action(|action| { + debug!("User action received: {}", action); + let mut result = action_result_clone + .lock() + .expect("Failed to lock action result"); + *result = Some(action.to_string()); + }); + + let action = action_result + .lock() + .expect("Failed to lock action result") + .clone() + .unwrap_or_else(|| "__closed".to_string()); + + let accepted = match action.as_str() { + "approve" => true, + "deny" => false, + "default" => default_means_accept, + "__closed" => false, + other => { + debug!("Unknown action '{}' - treating as denied", other); + false + } + }; + + if accepted { + debug!("User verification accepted via notification"); + Ok(VerificationResult::Accepted) + } else { + debug!("User verification denied or notification closed"); + Ok(VerificationResult::Denied) + } + } +} + +impl UserVerificationProvider for NotificationProvider { + fn name(&self) -> &str { + "notification" + } + + fn available(&self) -> bool { + true + } + + fn verify( + &self, + context: &VerificationContext, + ) -> Result { + self.show_notification(context) + } + + fn priority(&self) -> u8 { + 10 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_notification_provider_always_available() { + let provider = NotificationProvider::new(30); + assert!(provider.available()); + } + + #[test] + fn test_notification_provider_priority() { + let provider = NotificationProvider::new(30); + assert_eq!(provider.priority(), 10); + } + + #[test] + fn test_notification_provider_name() { + let provider = NotificationProvider::new(30); + assert_eq!(provider.name(), "notification"); + } +} diff --git a/docs/adr/001-biometric-user-verification.md b/docs/adr/001-biometric-user-verification.md new file mode 100644 index 00000000..71694182 --- /dev/null +++ b/docs/adr/001-biometric-user-verification.md @@ -0,0 +1,104 @@ +# ADR 001: Biometric User Verification + +## Status + +Accepted + +## Context + +Passless currently uses desktop notifications for user verification (UV) in FIDO2 operations. This is a "something you have" factor (the device with passless running). To provide true multi-factor authentication, we need "something you are" - biometric verification. + +Linux provides several options for biometric authentication: + +1. **fprintd** - Standard Linux fingerprint daemon with D-Bus interface +2. **Webcam face recognition** - Using modern ML models via ONNX +3. **External commands** - Allow users to integrate any biometric tool + +## Decision + +We will implement a pluggable user verification system with multiple providers: + +### Architecture + +``` +UserVerificationManager (chain of responsibility) +├── FprintdProvider (fingerprint via D-Bus) +├── FaceIdProvider (webcam + face recognition) +├── NotificationProvider (fallback, current behavior) +└── CommandProvider (user-defined script) +``` + +### Technology Choices + +1. **D-Bus Integration**: Use `zbus` crate (pure Rust, async-native) +2. **Webcam Capture**: Use `nokhwa` crate (cross-platform, no OpenCV dependency) +3. **Face Recognition**: Use `face_id` crate (SCRFD detection + ArcFace embeddings via ONNX Runtime) +4. **Provider Selection**: Configurable chain with automatic fallback + +### Configuration + +```toml +[uv] +providers = ["fprintd", "face", "notification"] # Try in order + +[uv.fprintd] +timeout_seconds = 30 + +[uv.face] +enabled = true +camera_index = 0 +threshold = 0.6 +``` + +## Consequences + +### Positive + +- True biometric verification for FIDO2 operations +- Multiple provider options for different hardware +- Automatic fallback ensures users aren't locked out +- Modern Rust stack (zbus, face_id) with minimal C dependencies + +### Negative + +- Increased binary size and dependencies +- face_id downloads ONNX models from HuggingFace on first use +- Webcam access requires proper permissions +- fprintd requires system-level setup + +### Neutral + +- Face recognition provider is feature-gated (`--features face`) to reduce binary size +- Other providers (fprintd, notification, command) are always included +- Face embeddings stored locally in user's storage backend + +## Implementation + +1. Create `uv/` module with `UserVerificationProvider` trait +2. Implement provider chain in `UserVerificationManager` +3. Implement each provider (fprintd, face, notification, command) +4. Update authenticator to use manager instead of direct notifications + +### Build Options + +```bash +# Standard build (fprintd + notification providers) +cargo build + +# With face recognition support (adds ~50MB+ due to ONNX Runtime) +cargo build --features face +``` + +## Alternatives Considered + +1. **All providers feature-gated**: Rejected - fprintd and notification are lightweight enough to include by default +2. **Howdy integration**: Rejected - Python-based, command provider allows users to integrate it themselves +3. **OpenCV for face recognition**: Rejected - heavy C++ dependency, face_id uses ONNX Runtime instead +4. **PAM integration**: Rejected - indirect, requires PAM conversation setup + +## References + +- [fprintd D-Bus API](https://fprint.freedesktop.org/) +- [nokhwa crate](https://crates.io/crates/nokhwa) +- [face_id crate](https://crates.io/crates/face_id) +- [zbus crate](https://crates.io/crates/zbus) diff --git a/passless-core/src/config.rs b/passless-core/src/config.rs index 1ecd8878..e82be973 100644 --- a/passless-core/src/config.rs +++ b/passless-core/src/config.rs @@ -283,6 +283,193 @@ pub struct PinConfig { pub auto_lock_timeout: u32, } +/// User verification provider type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum UvProviderType { + /// Fingerprint verification via fprintd D-Bus service + Fprintd, + /// Face recognition via webcam + Face, + /// Desktop notification (fallback) + Notification, + /// Custom command/script + Command, + /// Use all available providers in priority order + #[default] + Auto, +} + +impl std::str::FromStr for UvProviderType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "fprintd" | "fingerprint" => Ok(UvProviderType::Fprintd), + "face" | "faceid" => Ok(UvProviderType::Face), + "notification" | "notify" => Ok(UvProviderType::Notification), + "command" | "cmd" => Ok(UvProviderType::Command), + "auto" | "all" => Ok(UvProviderType::Auto), + _ => Err(format!( + "Invalid UV provider '{}'. Must be: fprintd, face, notification, command, or auto", + s + )), + } + } +} + +impl std::fmt::Display for UvProviderType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UvProviderType::Fprintd => write!(f, "fprintd"), + UvProviderType::Face => write!(f, "face"), + UvProviderType::Notification => write!(f, "notification"), + UvProviderType::Command => write!(f, "command"), + UvProviderType::Auto => write!(f, "auto"), + } + } +} + +/// fprintd provider configuration +#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)] +#[group(id = "uv-fprintd")] +pub struct FprintdConfig { + /// Enable fprintd provider + #[arg( + long = "fprintd-enabled", + id = "fprintd-enabled", + env = "PASSLESS_FPRINTD_ENABLED", + action = ArgAction::Set, + require_equals = true, + num_args = 0..=1, + default_missing_value = "true" + )] + #[serde(default)] + #[default(true)] + pub enabled: bool, +} + +/// Face recognition provider configuration +#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)] +#[group(id = "uv-face")] +pub struct FaceConfig { + /// Enable face recognition provider + #[arg( + long = "face-enabled", + id = "face-enabled", + env = "PASSLESS_FACE_ENABLED", + action = ArgAction::Set, + require_equals = true, + num_args = 0..=1, + default_missing_value = "true" + )] + #[serde(default)] + #[default(true)] + pub enabled: bool, + + /// Camera device index (0 = first camera) + #[arg( + long = "face-camera-index", + id = "face-camera-index", + env = "PASSLESS_FACE_CAMERA_INDEX", + value_name = "INDEX" + )] + #[serde(default)] + #[default(0)] + pub camera_index: usize, + + /// Face matching threshold (0.0-1.0, higher = stricter) + #[arg( + long = "face-threshold", + id = "face-threshold", + env = "PASSLESS_FACE_THRESHOLD", + value_name = "THRESHOLD" + )] + #[serde(default)] + #[default(0.6)] + pub threshold: f32, +} + +/// Command provider configuration +#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)] +#[group(id = "uv-cmd")] +pub struct CommandConfig { + /// Enable command provider + #[arg( + long = "uv-command-enabled", + id = "uv-command-enabled", + env = "PASSLESS_UV_COMMAND_ENABLED", + action = ArgAction::Set, + require_equals = true, + num_args = 0..=1, + default_missing_value = "false" + )] + #[serde(default)] + #[default(false)] + pub enabled: bool, + + /// Command to execute for verification (exit 0 = accept, non-zero = deny) + /// Command receives context via environment variables: + /// PASSLESS_UV_OPERATION, PASSLESS_UV_RELYING_PARTY, PASSLESS_UV_USER + #[arg( + long = "uv-command", + id = "uv-command-arg", + env = "PASSLESS_UV_COMMAND", + value_name = "COMMAND", + value_delimiter = ' ' + )] + #[serde(default)] + pub command: Vec, +} + +/// User verification configuration +#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)] +#[group(id = "uv")] +pub struct UvConfig { + /// User verification provider selection + /// - "auto": Use all available providers in priority order (default) + /// - "fprintd": Fingerprint only + /// - "face": Face recognition only + /// - "notification": Desktop notification only + /// - "command": Custom command only + #[arg( + long = "uv-provider", + env = "PASSLESS_UV_PROVIDER", + value_name = "PROVIDER" + )] + #[serde(default)] + #[default(UvProviderType::Auto)] + pub provider: UvProviderType, + + /// Default timeout for verification (seconds) + #[arg( + long = "uv-timeout", + env = "PASSLESS_UV_TIMEOUT", + value_name = "SECONDS" + )] + #[serde(default)] + #[default(30)] + pub timeout_seconds: u32, + + /// fprintd configuration + #[clap_serde] + #[serde(default)] + #[command(flatten)] + pub fprintd: FprintdConfig, + + /// Face recognition configuration + #[clap_serde] + #[serde(default)] + #[command(flatten)] + pub face: FaceConfig, + + /// Command provider configuration + #[clap_serde] + #[serde(default)] + #[command(flatten)] + pub command: CommandConfig, +} + impl SecurityConfig { /// Apply security hardening measures pub fn apply_hardening(&self) -> Result<(), Box> { @@ -384,6 +571,12 @@ pub struct AppConfig { #[serde(default)] #[command(flatten)] pub pin: PinConfig, + + /// User verification configuration + #[clap_serde] + #[serde(default)] + #[command(flatten)] + pub uv: UvConfig, } /// Backend-specific configuration @@ -471,6 +664,11 @@ impl AppConfig { pub fn pin_config(&self) -> PinConfig { self.pin.clone() } + + /// Get user verification configuration + pub fn uv_config(&self) -> UvConfig { + self.uv.clone() + } } /// CLI arguments structure From 71eaef7f0f1b3c6e83163adab36cc2171d6e3847 Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Thu, 12 Mar 2026 22:52:38 +0100 Subject: [PATCH 2/8] refactor: improve UV module code quality and maintainability - Use thiserror derive for VerificationError (removes boilerplate) - Define priority constants module (FPRINTD=100, FACE=90, COMMAND=50, NOTIFICATION=10) - Add config validation for face threshold (0.0-1.0 range) - Cache tokio runtime in FprintdProvider (reuse across verify calls) - Add from_config factory methods to all providers - Simplify build_uv_manager with extend pattern --- Cargo.lock | 1 + cmd/passless/Cargo.toml | 1 + cmd/passless/src/authenticator.rs | 56 ++++++-------- cmd/passless/src/main.rs | 6 ++ cmd/passless/src/uv/command.rs | 21 +++++- cmd/passless/src/uv/face.rs | 21 +++++- cmd/passless/src/uv/fprintd.rs | 56 +++++++++----- cmd/passless/src/uv/mod.rs | 113 ++++++++++++++++++++++------ cmd/passless/src/uv/notification.rs | 13 +++- passless-core/src/config.rs | 19 +++++ 10 files changed, 221 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90a06276..604f1c9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3276,6 +3276,7 @@ dependencies = [ "soft-fido2-ctap", "soft-fido2-transport", "tempfile", + "thiserror 2.0.18", "tokio", "tss-esapi", "which 7.0.3", diff --git a/cmd/passless/Cargo.toml b/cmd/passless/Cargo.toml index 7d6bdb19..468aa2ce 100644 --- a/cmd/passless/Cargo.toml +++ b/cmd/passless/Cargo.toml @@ -42,6 +42,7 @@ zeroize.workspace = true hex.workspace = true shadow-rs.workspace = true dirs.workspace = true +thiserror.workspace = true hmac = "0.12" rpassword = "7.3" atty = "0.2" diff --git a/cmd/passless/src/authenticator.rs b/cmd/passless/src/authenticator.rs index ef0679ab..84f8619a 100644 --- a/cmd/passless/src/authenticator.rs +++ b/cmd/passless/src/authenticator.rs @@ -2,8 +2,8 @@ use crate::pin_storage::PinStorage; use crate::storage::{CredentialFilter, CredentialStorage}; use crate::util::bytes_to_hex; use crate::uv::{ - FprintdProvider, NotificationProvider, UserVerificationManager, UserVerificationProvider, - VerificationContext, VerificationResult, + CommandProvider, FprintdProvider, NotificationProvider, UserVerificationManager, + UserVerificationProvider, VerificationContext, VerificationResult, }; #[cfg(feature = "face")] @@ -141,49 +141,37 @@ fn build_uv_manager(config: &UvConfig) -> UserVerificationManager { match config.provider { UvProviderType::Auto => { - if config.fprintd.enabled { - providers.push(Box::new(FprintdProvider::new())); - } + providers.extend(FprintdProvider::from_config(config.fprintd.enabled)); #[cfg(feature = "face")] - if config.face.enabled { - providers.push(Box::new(FaceIdProvider::new( - config.face.camera_index, - config.face.threshold, - None, - ))); - } - providers.push(Box::new(NotificationProvider::new(config.timeout_seconds))); + providers.extend(FaceIdProvider::from_config( + config.face.enabled, + config.face.camera_index, + config.face.threshold, + )); + providers.push(NotificationProvider::from_config(config.timeout_seconds)); } UvProviderType::Fprintd => { - if config.fprintd.enabled { - providers.push(Box::new(FprintdProvider::new())); - } + providers.extend(FprintdProvider::from_config(config.fprintd.enabled)); } UvProviderType::Face => { #[cfg(feature = "face")] - if config.face.enabled { - providers.push(Box::new(FaceIdProvider::new( - config.face.camera_index, - config.face.threshold, - None, - ))); - } + providers.extend(FaceIdProvider::from_config( + config.face.enabled, + config.face.camera_index, + config.face.threshold, + )); #[cfg(not(feature = "face"))] - { - log::warn!("Face provider requested but 'face' feature not enabled"); - } + log::warn!("Face provider requested but 'face' feature not enabled"); } UvProviderType::Notification => { - providers.push(Box::new(NotificationProvider::new(config.timeout_seconds))); + providers.push(NotificationProvider::from_config(config.timeout_seconds)); } UvProviderType::Command => { - if config.command.enabled && !config.command.command.is_empty() { - use crate::uv::CommandProvider; - providers.push(Box::new(CommandProvider::new( - config.command.command.clone(), - config.timeout_seconds, - ))); - } + providers.extend(CommandProvider::from_config( + config.command.enabled, + config.command.command.clone(), + config.timeout_seconds, + )); } } diff --git a/cmd/passless/src/main.rs b/cmd/passless/src/main.rs index 388cc32c..ad70f477 100644 --- a/cmd/passless/src/main.rs +++ b/cmd/passless/src/main.rs @@ -242,6 +242,12 @@ fn run() -> Result<()> { // Load config: CLI args + config file + defaults (CLI takes precedence) let config = AppConfig::load(&mut args); + // Validate configuration + if let Err(e) = config.validate() { + error!("Configuration validation failed: {}", e); + process::exit(1); + } + if config.verbose && log_level != log::LevelFilter::Debug { info!("Enabling verbose logging..."); log::set_max_level(log::LevelFilter::Debug); diff --git a/cmd/passless/src/uv/command.rs b/cmd/passless/src/uv/command.rs index 408255ff..9f82f895 100644 --- a/cmd/passless/src/uv/command.rs +++ b/cmd/passless/src/uv/command.rs @@ -16,7 +16,9 @@ //! - `PASSLESS_UV_USER`: User identifier (if available) //! - `PASSLESS_UV_TIMEOUT`: Timeout in seconds -use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; +use super::{ + UserVerificationProvider, VerificationContext, VerificationError, VerificationResult, priority, +}; use log::{debug, info}; @@ -43,6 +45,19 @@ impl CommandProvider { } } + /// Create a provider if enabled and configured + pub fn from_config( + enabled: bool, + command: Vec, + timeout_seconds: u32, + ) -> Option> { + if enabled && !command.is_empty() { + Some(Box::new(Self::new(command, timeout_seconds))) + } else { + None + } + } + /// Execute the verification command fn execute( &self, @@ -129,7 +144,7 @@ impl UserVerificationProvider for CommandProvider { } fn priority(&self) -> u8 { - 50 + priority::COMMAND } } @@ -146,7 +161,7 @@ mod tests { #[test] fn test_command_provider_priority() { let provider = CommandProvider::new(vec!["echo".to_string()], 30); - assert_eq!(provider.priority(), 50); + assert_eq!(provider.priority(), priority::COMMAND); } #[test] diff --git a/cmd/passless/src/uv/face.rs b/cmd/passless/src/uv/face.rs index 7116f386..74198664 100644 --- a/cmd/passless/src/uv/face.rs +++ b/cmd/passless/src/uv/face.rs @@ -13,7 +13,9 @@ //! During enrollment, multiple face embeddings are captured and averaged //! to create a robust template. The template is stored securely. -use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; +use super::{ + UserVerificationProvider, VerificationContext, VerificationError, VerificationResult, priority, +}; use log::{debug, info, warn}; @@ -58,6 +60,19 @@ impl FaceIdProvider { } } + /// Create a provider if enabled in config + pub fn from_config( + enabled: bool, + camera_index: usize, + threshold: f32, + ) -> Option> { + if enabled { + Some(Box::new(Self::new(camera_index, threshold, None))) + } else { + None + } + } + /// Get path to the stored embeddings file fn embeddings_file(&self) -> PathBuf { self.embeddings_path.join("face_template.bin") @@ -275,7 +290,7 @@ impl UserVerificationProvider for FaceIdProvider { } fn priority(&self) -> u8 { - 90 + priority::FACE } fn supports_enrollment(&self) -> bool { @@ -358,7 +373,7 @@ mod tests { #[test] fn test_face_provider_priority() { let provider = FaceIdProvider::new(0, DEFAULT_THRESHOLD, None); - assert_eq!(provider.priority(), 90); + assert_eq!(provider.priority(), priority::FACE); } #[test] diff --git a/cmd/passless/src/uv/fprintd.rs b/cmd/passless/src/uv/fprintd.rs index 64eb4e93..bfec51f4 100644 --- a/cmd/passless/src/uv/fprintd.rs +++ b/cmd/passless/src/uv/fprintd.rs @@ -8,25 +8,50 @@ //! - A fingerprint reader must be connected //! - User must have enrolled fingerprints -use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; +use super::{ + UserVerificationProvider, VerificationContext, VerificationError, VerificationResult, priority, +}; use log::{debug, error, info, warn}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::Mutex; /// fprintd-based fingerprint verification provider pub struct FprintdProvider { - available_cache: Arc>>, + available_cache: Mutex>, + runtime: Mutex>>, } impl FprintdProvider { /// Create a new fprintd provider pub fn new() -> Self { Self { - available_cache: Arc::new(Mutex::new(None)), + available_cache: Mutex::new(None), + runtime: Mutex::new(None), } } + /// Create a provider if enabled in config + pub fn from_config(enabled: bool) -> Option> { + if enabled { + Some(Box::new(Self::new())) + } else { + None + } + } + + /// Get or create the tokio runtime + fn get_runtime(&self) -> Result, VerificationError> { + let mut runtime_guard = self.runtime.lock().unwrap(); + if runtime_guard.is_none() { + *runtime_guard = Some(Arc::new(tokio::runtime::Runtime::new().map_err(|e| { + VerificationError::DeviceError(format!("Failed to create runtime: {}", e)) + })?)); + } + Ok(runtime_guard.as_ref().unwrap().clone()) + } + async fn do_verify(timeout_secs: u32) -> Result { use zbus::Connection; @@ -195,8 +220,8 @@ impl FprintdProvider { Ok(result) } - fn check_available_sync() -> bool { - let rt = match tokio::runtime::Runtime::new() { + fn check_available(&self) -> bool { + let rt = match self.get_runtime() { Ok(rt) => rt, Err(_) => return false, }; @@ -252,7 +277,7 @@ impl UserVerificationProvider for FprintdProvider { return cached; } - let available = Self::check_available_sync(); + let available = self.check_available(); *self.available_cache.lock().unwrap() = Some(available); available } @@ -262,22 +287,13 @@ impl UserVerificationProvider for FprintdProvider { context: &VerificationContext, ) -> Result { let timeout_secs = context.timeout_seconds; + let rt = self.get_runtime()?; - let result = std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| { - VerificationError::DeviceError(format!("Failed to create runtime: {}", e)) - })?; - - rt.block_on(async { Self::do_verify(timeout_secs).await }) - }) - .join() - .map_err(|_| VerificationError::Other("Verification thread panicked".into()))??; - - Ok(result) + rt.block_on(async { Self::do_verify(timeout_secs).await }) } fn priority(&self) -> u8 { - 100 + priority::FPRINTD } fn supports_enrollment(&self) -> bool { @@ -306,7 +322,7 @@ mod tests { #[test] fn test_fprintd_provider_priority() { let provider = FprintdProvider::new(); - assert_eq!(provider.priority(), 100); + assert_eq!(provider.priority(), priority::FPRINTD); } #[test] diff --git a/cmd/passless/src/uv/mod.rs b/cmd/passless/src/uv/mod.rs index f802cf62..a97ff104 100644 --- a/cmd/passless/src/uv/mod.rs +++ b/cmd/passless/src/uv/mod.rs @@ -1,10 +1,39 @@ //! User Verification (UV) module for FIDO2 authenticator //! //! This module provides a pluggable user verification system with multiple providers: -//! - FprintdProvider: Fingerprint verification via D-Bus -//! - FaceIdProvider: Face recognition via webcam (requires "face" feature) -//! - NotificationProvider: Desktop notification (fallback) -//! - CommandProvider: Custom command/script execution +//! - [`FprintdProvider`]: Fingerprint verification via D-Bus +//! - [`FaceIdProvider`]: Face recognition via webcam (requires "face" feature) +//! - [`NotificationProvider`]: Desktop notification (fallback) +//! - [`CommandProvider`]: Custom command/script execution +//! +//! # Architecture +//! +//! Providers implement the [`UserVerificationProvider`] trait and are managed by +//! [`UserVerificationManager`], which handles priority ordering and automatic fallback. +//! +//! # Example +//! +//! ```ignore +//! use passless::uv::{ +//! UserVerificationManager, FprintdProvider, NotificationProvider, +//! VerificationContext, UserVerificationProvider, +//! }; +//! +//! let manager = UserVerificationManager::new(vec![ +//! Box::new(FprintdProvider::new()), +//! Box::new(NotificationProvider::new(30)), +//! ]); +//! +//! let ctx = VerificationContext::new("authentication") +//! .with_relying_party("example.com"); +//! +//! match manager.verify(&ctx) { +//! Ok(VerificationResult::Accepted) => { /* success */ } +//! Ok(VerificationResult::Denied) => { /* denied */ } +//! Ok(VerificationResult::Timeout) => { /* timeout */ } +//! Err(e) => { /* error */ } +//! } +//! ``` mod command; mod fprintd; @@ -14,8 +43,6 @@ mod notification; #[cfg(feature = "face")] mod face; -use std::fmt; - pub use command::CommandProvider; #[cfg(feature = "face")] pub use face::FaceIdProvider; @@ -23,6 +50,22 @@ pub use fprintd::FprintdProvider; pub use manager::UserVerificationManager; pub use notification::NotificationProvider; +/// Default timeout for user verification (seconds) +pub const DEFAULT_TIMEOUT_SECONDS: u32 = 30; + +/// Provider priority constants (higher = preferred) +pub mod priority { + /// Fingerprint provider priority (highest - most secure biometric) + pub const FPRINTD: u8 = 100; + /// Face recognition provider priority + #[cfg(feature = "face")] + pub const FACE: u8 = 90; + /// Custom command provider priority (configurable) + pub const COMMAND: u8 = 50; + /// Notification provider priority (lowest - fallback) + pub const NOTIFICATION: u8 = 10; +} + /// Result of a user verification attempt #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VerificationResult { @@ -35,34 +78,54 @@ pub enum VerificationResult { } /// Error during user verification -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum VerificationError { /// Provider is not available (not installed, no device, etc.) + #[error("Not available: {0}")] NotAvailable(String), /// Device or system error during verification + #[error("Device error: {0}")] DeviceError(String), /// User cancelled the operation + #[error("User cancelled")] #[allow(dead_code)] UserCancelled, /// Provider is not enrolled (no biometric data registered) + #[error("Not enrolled")] NotEnrolled, /// Unknown error + #[error("Error: {0}")] Other(String), } -impl fmt::Display for VerificationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - VerificationError::NotAvailable(msg) => write!(f, "Not available: {}", msg), - VerificationError::DeviceError(msg) => write!(f, "Device error: {}", msg), - VerificationError::UserCancelled => write!(f, "User cancelled"), - VerificationError::NotEnrolled => write!(f, "Not enrolled"), - VerificationError::Other(msg) => write!(f, "Error: {}", msg), +impl From for VerificationError { + fn from(err: std::io::Error) -> Self { + VerificationError::DeviceError(err.to_string()) + } +} + +impl From for VerificationError { + fn from(err: zbus::Error) -> Self { + match err { + zbus::Error::MethodError(_, _, _) => { + VerificationError::DeviceError(format!("D-Bus method failed: {}", err)) + } + zbus::Error::InterfaceNotFound => { + VerificationError::NotAvailable("D-Bus interface not found".into()) + } + zbus::Error::FDO(e) => { + VerificationError::DeviceError(format!("D-Bus freedesktop error: {}", e)) + } + _ => VerificationError::DeviceError(format!("D-Bus error: {}", err)), } } } -impl std::error::Error for VerificationError {} +impl From for VerificationError { + fn from(err: zbus::zvariant::Error) -> Self { + VerificationError::DeviceError(format!("D-Bus variant error: {}", err)) + } +} /// Context for user verification request #[derive(Debug, Clone)] @@ -84,7 +147,7 @@ impl VerificationContext { operation: operation.into(), relying_party: None, user: None, - timeout_seconds: 30, + timeout_seconds: DEFAULT_TIMEOUT_SECONDS, } } @@ -132,12 +195,9 @@ pub trait UserVerificationProvider: Send + Sync { /// Priority of this provider (higher = preferred) /// - /// Default priorities: - /// - fprintd: 100 - /// - face: 90 - /// - notification: 10 + /// See [`priority`] module for standard values. fn priority(&self) -> u8 { - 50 + priority::COMMAND } /// Whether this provider supports enrollment @@ -174,7 +234,7 @@ mod tests { assert_eq!(ctx.operation, "registration"); assert_eq!(ctx.relying_party, None); assert_eq!(ctx.user, None); - assert_eq!(ctx.timeout_seconds, 30); + assert_eq!(ctx.timeout_seconds, DEFAULT_TIMEOUT_SECONDS); } #[test] @@ -210,4 +270,11 @@ mod tests { "Error: oops" ); } + + #[test] + fn test_verification_error_from_io() { + let err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); + let uv_err: VerificationError = err.into(); + assert!(matches!(uv_err, VerificationError::DeviceError(_))); + } } diff --git a/cmd/passless/src/uv/notification.rs b/cmd/passless/src/uv/notification.rs index c4628212..745fc4ac 100644 --- a/cmd/passless/src/uv/notification.rs +++ b/cmd/passless/src/uv/notification.rs @@ -3,7 +3,9 @@ //! Uses desktop notifications for user verification. //! This is the fallback provider when no biometric methods are available. -use super::{UserVerificationProvider, VerificationContext, VerificationError, VerificationResult}; +use super::{ + UserVerificationProvider, VerificationContext, VerificationError, VerificationResult, priority, +}; use log::debug; @@ -47,6 +49,11 @@ impl NotificationProvider { Self { timeout_seconds } } + /// Create a provider with timeout + pub fn from_config(timeout_seconds: u32) -> Box { + Box::new(Self::new(timeout_seconds)) + } + /// Show verification notification and wait for response fn show_notification( &self, @@ -136,7 +143,7 @@ impl UserVerificationProvider for NotificationProvider { } fn priority(&self) -> u8 { - 10 + priority::NOTIFICATION } } @@ -153,7 +160,7 @@ mod tests { #[test] fn test_notification_provider_priority() { let provider = NotificationProvider::new(30); - assert_eq!(provider.priority(), 10); + assert_eq!(provider.priority(), priority::NOTIFICATION); } #[test] diff --git a/passless-core/src/config.rs b/passless-core/src/config.rs index e82be973..04740d63 100644 --- a/passless-core/src/config.rs +++ b/passless-core/src/config.rs @@ -390,6 +390,19 @@ pub struct FaceConfig { pub threshold: f32, } +impl FaceConfig { + /// Validate the configuration + pub fn validate(&self) -> Result<(), String> { + if self.threshold < 0.0 || self.threshold > 1.0 { + return Err(format!( + "Face threshold must be between 0.0 and 1.0, got {}", + self.threshold + )); + } + Ok(()) + } +} + /// Command provider configuration #[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)] #[group(id = "uv-cmd")] @@ -669,6 +682,12 @@ impl AppConfig { pub fn uv_config(&self) -> UvConfig { self.uv.clone() } + + /// Validate the configuration + pub fn validate(&self) -> Result<(), String> { + self.uv.face.validate()?; + Ok(()) + } } /// CLI arguments structure From 9bae25b895d84acbf56cc0a405e3b10e3105c58e Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Mon, 23 Mar 2026 12:03:46 +0000 Subject: [PATCH 3/8] fix: pin Rust to 1.93 for Ubuntu 22.04 glibc compatibility Rust 1.94.0 requires glibc symbols (__isoc23_strtol*) that are not available in Ubuntu 22.04 (glibc 2.35). Pin to Rust 1.93.0 for the Ubuntu 22.04 build target to maintain compatibility with older glibc. Also switched from deprecated actions-rs/toolchain to dtolnay/rust-toolchain. --- .github/workflows/rust.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f6f5ee4c..3cd54069 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -73,11 +73,16 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable + with: + target: ${{ matrix.job.target }} + if: matrix.job.os != 'ubuntu-22.04' + + - name: Install Rust toolchain (pinned for older glibc) + uses: dtolnay/rust-toolchain@1.93.0 with: - toolchain: stable target: ${{ matrix.job.target }} - override: true + if: matrix.job.os == 'ubuntu-22.04' - name: Build shell: bash From b2065ba6990f6c71567101fb09a09c00568dd8dd Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Mon, 23 Mar 2026 12:52:19 +0000 Subject: [PATCH 4/8] fix: add merge to allowed commit types in commitlint config --- .commitlintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.commitlintrc.json b/.commitlintrc.json index d3fcfc22..38c7b200 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -12,6 +12,7 @@ "docs", "feat", "fix", + "merge", "refactor", "release", "revert", From 7de53ffe9b60edda90aa54dfd6f9472f86b91583 Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Mon, 23 Mar 2026 12:59:34 +0000 Subject: [PATCH 5/8] fix: exclude face feature from CI builds on older glibc The face feature requires glibc 2.38+ (for __isoc23_strtoll symbols) but Ubuntu 22.04 has glibc 2.35. Changed CI and Makefile to use --features tpm instead of --all-features for compatibility. --- .github/workflows/rust.yml | 6 +++--- Makefile | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3cd54069..ef188fc1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,7 +49,7 @@ jobs: - uses: clechasseur/rs-clippy-check@v5 with: - args: --all-features -- --deny warnings + args: --features tpm -- --deny warnings build: strategy: @@ -86,11 +86,11 @@ jobs: - name: Build shell: bash - run: cargo build --verbose + run: cargo build --verbose --features tpm - name: Run tests shell: bash - run: cargo test --verbose + run: cargo test --verbose --features tpm - name: Extract crate information shell: bash diff --git a/Makefile b/Makefile index 26536773..34d70cc4 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ CARGO_TARGET_DIR ?= target CARGO_TARGET ?= x86_64-unknown-linux-gnu PKG_BASE_NAME ?= passless-${CARGO_TARGET} PROJECT_VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' ./Cargo.toml | head -n1) +# Default to tpm feature only, face feature requires glibc 2.38+ +CARGO_FEATURES ?= tpm .DEFAULT: help .PHONY: help @@ -77,7 +79,7 @@ update-changelog: ## automatically update changelog based on commits release: ## generate vendor.tar.gz, $(PKG_BASE_NAME).tar.gz, and completions cargo vendor tar -czf vendor.tar.gz vendor - cargo build --frozen --release --all-features --target ${CARGO_TARGET} + cargo build --frozen --release --features ${CARGO_FEATURES} --target ${CARGO_TARGET} tar -czf $(PKG_BASE_NAME).tar.gz -C $(CARGO_TARGET_DIR)/$(CARGO_TARGET)/release passless @# Create completions tarball @COMPLETION_DIR=$$(find $(CARGO_TARGET_DIR)/$(CARGO_TARGET)/release/build/passless-*/out/completions -type d 2>/dev/null | head -1); \ From 17673e104635546adae6ca2c6ac2259cb7c8ac08 Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Fri, 29 May 2026 17:00:34 +0000 Subject: [PATCH 6/8] fix: correct YAML indentation in pre-commit config --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccc0468c..360acbc1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,8 +16,8 @@ repos: hooks: - id: yamllint - - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook -rev: v9.25.0 +- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.25.0 hooks: - id: commitlint stages: [commit-msg] From 16d6dc1e037a913f5bd6fe708381e14335363c0c Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Fri, 29 May 2026 17:07:13 +0000 Subject: [PATCH 7/8] fix: correct YAML indentation in pre-commit config --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 360acbc1..88f930ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: yamllint -- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.25.0 hooks: - id: commitlint From 6b651e5d3f6c6aa4804a49b5e352357f80ba5499 Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Fri, 29 May 2026 17:15:09 +0000 Subject: [PATCH 8/8] fix: correct YAML indentation and add trailing newlines --- .github/workflows/aur-publish.yml | 2 +- .github/workflows/rust.yml | 6 +++--- cmd/passless/src/authenticator.rs | 2 +- cmd/passless/src/pin_storage/pass.rs | 2 +- passless-core/src/config.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index a13f412e..677040ef 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -43,7 +43,7 @@ jobs: | tar xvz -C /usr/local/bin .ci/generate-pkgbuild.rh ${{ matrix.package.pkgbuild }} - uses: KSXGitHub/github-actions-deploy-aur@v4.1.3 + - uses: KSXGitHub/github-actions-deploy-aur@v4.1.3 with: pkgname: ${{ matrix.package.name }} pkgbuild: ./${{ matrix.package.pkgbuild }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2f9a1db8..5ed06155 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -47,7 +47,7 @@ jobs: - run: rustup override set ${{steps.toolchain.outputs.name}} - run: rustup component add clippy -- uses: clechasseur/rs-clippy-check@v6.0.4 + - uses: clechasseur/rs-clippy-check@v6.0.4 with: args: --features tpm -- --deny warnings @@ -130,7 +130,7 @@ jobs: - name: Get Changelog Entry if: startsWith(github.ref, 'refs/tags/') id: changelog_reader -uses: mindsers/changelog-reader-action@v2.4.0 + uses: mindsers/changelog-reader-action@v2.4.0 with: version: v${{ env.PROJECT_VERSION }} path: ./CHANGELOG.md @@ -151,7 +151,7 @@ uses: mindsers/changelog-reader-action@v2.4.0 echo "EOF" >> $GITHUB_OUTPUT - name: Publish release -uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@v3 if: startsWith(github.ref, 'refs/tags/') with: body: ${{ steps.changelog_reader.outputs.changes }} diff --git a/cmd/passless/src/authenticator.rs b/cmd/passless/src/authenticator.rs index 14509a3f..388f6345 100644 --- a/cmd/passless/src/authenticator.rs +++ b/cmd/passless/src/authenticator.rs @@ -664,4 +664,4 @@ mod tests { let _ = std::fs::remove_dir_all(temp_dir); } -} \ No newline at end of file +} diff --git a/cmd/passless/src/pin_storage/pass.rs b/cmd/passless/src/pin_storage/pass.rs index d3e9a61a..2ff89031 100644 --- a/cmd/passless/src/pin_storage/pass.rs +++ b/cmd/passless/src/pin_storage/pass.rs @@ -520,4 +520,4 @@ impl PinStorage for PassPinStorage { Ok(()) } -} \ No newline at end of file +} diff --git a/passless-core/src/config.rs b/passless-core/src/config.rs index 6690613d..45ca1140 100644 --- a/passless-core/src/config.rs +++ b/passless-core/src/config.rs @@ -848,4 +848,4 @@ pub enum PinAction { #[arg(value_name = "NEW_PIN")] new_pin: String, }, -} \ No newline at end of file +}