diff --git a/Cargo.lock b/Cargo.lock index f357b4b..e6507e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,45 +196,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -272,9 +233,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -603,9 +564,9 @@ dependencies = [ [[package]] name = "blade-graphics" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae" +checksum = "e71cfb73b98eb9f58ee84048aa1bdf4e7497fd20c141b57523499fa066b48fed" dependencies = [ "ash", "ash-window", @@ -873,9 +834,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -884,33 +845,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1001,7 +935,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.2", + "unicode-width", ] [[package]] @@ -1318,29 +1252,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctap-hid-fido2" -version = "3.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab54a72e056ad648b432faab0782512a989074a0e15d7c01e24825b22b0d033" -dependencies = [ - "aes", - "anyhow", - "base64", - "byteorder", - "cbc", - "ciborium", - "hex", - "hidapi", - "num", - "pad", - "rand 0.9.2", - "ring", - "strum 0.27.2", - "strum_macros 0.27.2", - "x509-parser", -] - [[package]] name = "ctor" version = "0.4.3" @@ -1357,12 +1268,6 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - [[package]] name = "data-url" version = "0.3.2" @@ -1371,32 +1276,9 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "deflate64" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" - -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] +checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "derive_more" @@ -1520,9 +1402,9 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "objc2", @@ -1541,9 +1423,9 @@ dependencies = [ [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ "libloading", ] @@ -1697,9 +1579,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -2180,20 +2062,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -2691,15 +2573,15 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hidapi" -version = "2.6.4" +version = "2.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565dd4c730b8f8b2c0fb36df6be12e5470ae10895ddcc4e9dcfbfb495de202b0" +checksum = "d1b71e1f4791fb9e93b9d7ee03d70b501ab48f6151432fbcadeabc30fe15396e" dependencies = [ "cc", "cfg-if", "libc", "pkg-config", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -3137,9 +3019,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is-docker" @@ -3205,9 +3087,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3333,13 +3215,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -3854,12 +3737,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - [[package]] name = "num-derive" version = "0.4.2" @@ -3945,9 +3822,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -4058,15 +3935,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -4150,15 +4018,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "pad" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" -dependencies = [ - "unicode-width 0.1.14", -] - [[package]] name = "parking" version = "2.2.1" @@ -4308,10 +4167,11 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" name = "picoforge" version = "0.4.1" dependencies = [ + "aes", "anyhow", "bitflags 2.11.0", "byteorder", - "ctap-hid-fido2", + "cbc", "directories", "gpui", "gpui-component", @@ -4332,18 +4192,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -4352,9 +4212,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4364,9 +4224,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand 2.3.0", @@ -4379,6 +4239,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "png" version = "0.17.16" @@ -4451,12 +4317,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4484,11 +4344,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.3+spec-1.1.0", ] [[package]] @@ -4553,12 +4413,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "qoi" @@ -4650,9 +4507,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4663,6 +4520,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -4691,7 +4554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -4863,9 +4726,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags 2.11.0", ] @@ -4937,9 +4800,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "resvg" @@ -4957,9 +4820,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ "bytemuck", ] @@ -5103,15 +4966,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "rustix" version = "0.38.44" @@ -5140,9 +4994,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -6032,12 +5886,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand 2.3.0", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -6127,37 +5981,6 @@ dependencies = [ "zune-jpeg 0.4.21", ] -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "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 = "tiny-keccak" version = "2.0.2" @@ -6220,9 +6043,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -6312,6 +6135,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -6328,12 +6160,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "a0a07913e63758bc95142d9863a5a45173b71515e68b690cad70cf99c3255ce1" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "winnow", ] @@ -6508,13 +6340,13 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -6595,12 +6427,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.2" @@ -6692,7 +6518,7 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "sha1_smol", @@ -6823,9 +6649,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -6836,9 +6662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -6850,9 +6676,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6860,9 +6686,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -6873,9 +6699,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -7026,9 +6852,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -7743,23 +7569,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "x509-parser" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom 7.1.3", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - [[package]] name = "xattr" version = "0.2.3" @@ -8092,18 +7901,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ffe47d2..dd26ef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2024" build = "build.rs" [dependencies] -# tokio = { version = "1.49", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" log = "0.4" # Logging facade @@ -21,12 +20,13 @@ hex = "0.4" # For parsing VID/PID strings byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement) thiserror = "2" # Makes custom error handling much easier anyhow = "1" # For easy error propagation -ctap-hid-fido2 = "3.5" # For fido2 interface operations hidapi = "2.6" # For fido2 interface operations but non-standard commands serde_cbor_2 = "0.13" rand = "0.10" bitflags = "2.11" ring = "0.17" # For signing fido2 messages with pin token +aes = "0.8" +cbc = "0.1" # For Application UI: gpui = { version = "0.2.2", features = [] } diff --git a/build.rs b/build.rs index 1aae248..2e1290d 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ #[cfg(windows)] +#[allow(clippy::single_component_path_imports)] use tauri_winres; // Configures windows application resource.( fix for app icon and launching app as admin) diff --git a/src/device/fido/constants.rs b/src/device/fido/constants.rs index 0c4801d..d2797f3 100644 --- a/src/device/fido/constants.rs +++ b/src/device/fido/constants.rs @@ -54,7 +54,7 @@ pub enum ClientPinSubCommand { GetPinToken = 0x05, GetPinUvAuthTokenUsingUvWithPermissions = 0x06, GetUvRetries = 0x07, - GetPinUvAuthTokenUsingPinWithPermissions = 0x08, + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // TODO: per fido spec, this should be 0x08? Needs to confirm and fix the firmware if true. } #[repr(u8)] @@ -97,6 +97,16 @@ pub enum ClientPinParam { PermissionsRpId = 0x0A, } +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClientPinResponseParam { + KeyAgreement = 0x01, + PinToken = 0x02, + PinRetries = 0x03, + NextMsg = 0x04, + UvRetries = 0x05, +} + #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigParam { @@ -133,6 +143,39 @@ pub enum VendorSubParam { VendorParamText = 0x04, } +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CredentialMgmtSubCommand { + GetCredsMetadata = 0x01, + EnumerateRpsBegin = 0x02, + EnumerateRpsGetNextRp = 0x03, + EnumerateCredentialsBegin = 0x04, + EnumerateCredentialsGetNextCredential = 0x05, + DeleteCredential = 0x06, + UpdateUserInformation = 0x07, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CredentialMgmtParam { + SubCommand = 0x01, + SubCommandParams = 0x02, + PinUvAuthProtocol = 0x03, + PinUvAuthParam = 0x04, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CredentialMgmtResponseParam { + Rp = 0x03, + RpIdHash = 0x04, + TotalRps = 0x05, + User = 0x06, + CredentialId = 0x07, + PublicKey = 0x08, + TotalCredentials = 0x09, +} + #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigSubCommandParam { @@ -141,32 +184,20 @@ pub enum ConfigSubCommandParam { ForceChangePin = 0x03, } +#[repr(u64)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VendorConfigCommand { - AuthEncryptionEnable, - AuthEncryptionDisable, - EnterpriseAttestationUpload, - PinComplexityPolicy, - PhysicalVidPid, - PhysicalLedBrightness, - PhysicalLedGpio, - PhysicalOptions, + AuthEncryptionEnable = 0x03e43f56b34285e2, + AuthEncryptionDisable = 0x1831a40f04a25ed9, + EnterpriseAttestationUpload = 0x66f2a674c29a8dcf, + PinComplexityPolicy = 0x6c07d70fe96c3897, + PhysicalVidPid = 0x6fcb19b0cbe3acfa, + PhysicalLedBrightness = 0x76a85945985d02fd, + PhysicalLedGpio = 0x7b392a394de9f948, + PhysicalOptions = 0x269f3b09eceb805f, } impl VendorConfigCommand { - pub fn to_u64(self) -> u64 { - match self { - Self::AuthEncryptionEnable => 0x03e43f56b34285e2, - Self::AuthEncryptionDisable => 0x1831a40f04a25ed9, - Self::EnterpriseAttestationUpload => 0x66f2a674c29a8dcf, - Self::PinComplexityPolicy => 0x6c07d70fe96c3897, - Self::PhysicalVidPid => 0x6fcb19b0cbe3acfa, - Self::PhysicalLedBrightness => 0x76a85945985d02fd, - Self::PhysicalLedGpio => 0x7b392a394de9f948, - Self::PhysicalOptions => 0x269f3b09eceb805f, - } - } - pub fn from_u64(val: u64) -> Option { match val { 0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable), @@ -182,6 +213,55 @@ impl VendorConfigCommand { } } +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FidoCertification { + AuthEncryption = 0x03E43F56B34285E2, + AuthEncryptionLock = 0x1831A40F04A25ED9, + EnterpriseAttestation = 0x66F2A674C29A8DCF, + PinComplexity = 0x6C07D70FE96C3897, + PhysicalVidPid = 0x6FCB19B0CBE3ACFA, + LedBrightness = 0x76A85945985D02FD, + LedGpio = 0x7B392A394DE9F948, + PhysicalOptions = 0x269F3B09ECEB805F, +} + +impl FidoCertification { + pub fn from_u64(val: u64) -> Option { + match val { + 0x03E43F56B34285E2 => Some(Self::AuthEncryption), + 0x1831A40F04A25ED9 => Some(Self::AuthEncryptionLock), + 0x66F2A674C29A8DCF => Some(Self::EnterpriseAttestation), + 0x6C07D70FE96C3897 => Some(Self::PinComplexity), + 0x6FCB19B0CBE3ACFA => Some(Self::PhysicalVidPid), + 0x76A85945985D02FD => Some(Self::LedBrightness), + 0x7B392A394DE9F948 => Some(Self::LedGpio), + 0x269F3B09ECEB805F => Some(Self::PhysicalOptions), + _ => None, + } + } + + pub fn from_str(val: &str) -> Option { + let val = val.strip_prefix("0x").unwrap_or(val); + u64::from_str_radix(val, 16).ok().and_then(Self::from_u64) + } +} + +impl fmt::Display for FidoCertification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthEncryption => write!(f, "Auth Encryption"), + Self::AuthEncryptionLock => write!(f, "Auth Encryption (Lock)"), + Self::EnterpriseAttestation => write!(f, "Enterprise Attestation"), + Self::PinComplexity => write!(f, "PIN Complexity"), + Self::PhysicalVidPid => write!(f, "Physical VID/PID"), + Self::LedBrightness => write!(f, "LED Brightness"), + Self::LedGpio => write!(f, "LED GPIO"), + Self::PhysicalOptions => write!(f, "Physical Options"), + } + } +} + impl fmt::Display for VendorConfigCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -239,6 +319,7 @@ pub enum MemoryResponseKey { } bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PinUvAuthTokenPermissions: u8 { const MAKE_CREDENTIAL = 0x01; const GET_ASSERTION = 0x02; @@ -288,6 +369,55 @@ pub enum CoseAlgorithm { ESB512 = -268, } +impl CoseAlgorithm { + pub fn from_i128(val: i128) -> Option { + match val as i32 { + -7 => Some(Self::ES256), + -8 => Some(Self::EdDSA), + -9 => Some(Self::ESP256), + -19 => Some(Self::Ed25519), + -25 => Some(Self::EcdhEsHkdf256), + -35 => Some(Self::ES384), + -36 => Some(Self::ES512), + -47 => Some(Self::ES256K), + -51 => Some(Self::ESP384), + -52 => Some(Self::ESP512), + -53 => Some(Self::Ed448), + -257 => Some(Self::RS256), + -258 => Some(Self::RS384), + -259 => Some(Self::RS512), + -265 => Some(Self::ESB256), + -267 => Some(Self::ESB384), + -268 => Some(Self::ESB512), + _ => None, + } + } +} + +impl fmt::Display for CoseAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ES256 => write!(f, "ES256"), + Self::EdDSA => write!(f, "EdDSA"), + Self::ESP256 => write!(f, "ESP256"), + Self::Ed25519 => write!(f, "Ed25519"), + Self::EcdhEsHkdf256 => write!(f, "ECDH-ES-HKDF-256"), + Self::ES384 => write!(f, "ES384"), + Self::ES512 => write!(f, "ES512"), + Self::ES256K => write!(f, "ES256K"), + Self::ESP384 => write!(f, "ESP384"), + Self::ESP512 => write!(f, "ESP512"), + Self::Ed448 => write!(f, "Ed448"), + Self::RS256 => write!(f, "RS256"), + Self::RS384 => write!(f, "RS384"), + Self::RS512 => write!(f, "RS512"), + Self::ESB256 => write!(f, "ESB256"), + Self::ESB384 => write!(f, "ESB384"), + Self::ESB512 => write!(f, "ESB512"), + } + } +} + #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseCurve { diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 5036335..f9eac07 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -1,5 +1,8 @@ +use aes::cipher::generic_array::GenericArray; +use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit, block_padding::NoPadding}; use rand::RngExt; -use serde_cbor_2::{Value, to_vec}; +use ring::{agreement, digest, hmac}; +use serde_cbor_2::{Value, from_slice, to_vec}; use std::collections::BTreeMap; use std::time::Duration; @@ -20,6 +23,7 @@ const HID_READ_TIMEOUT_MS: i32 = 10; const HID_INIT_READ_TIMEOUT_MS: i32 = 100; const HID_RESP_READ_TIMEOUT_MS: i32 = 2000; const HID_CONT_READ_TIMEOUT_MS: i32 = 500; +const HID_TOTAL_TIMEOUT_MS: i32 = 5000; pub struct HidTransport { device: hidapi::HidDevice, @@ -29,6 +33,24 @@ pub struct HidTransport { pub product_name: String, } +#[derive(Debug, Clone)] +pub struct EnumerateRpResponse { + pub rp: Value, + pub rp_id_hash: Vec, + #[allow(dead_code)] + pub total_rps: Option, +} + +#[derive(Debug, Clone)] +pub struct EnumerateCredentialResponse { + pub user: Value, + pub credential_id: Value, + #[allow(dead_code)] + pub public_key: Value, + #[allow(dead_code)] + pub total_credentials: Option, +} + impl HidTransport { pub fn open() -> Result { log::info!("Attempting to open HID transport for FIDO device..."); @@ -221,8 +243,18 @@ impl HidTransport { let mut read_len = 0; let mut last_seq = 0; + let start_time = std::time::Instant::now(); + let timeout_duration = std::time::Duration::from_millis(HID_TOTAL_TIMEOUT_MS as u64); + // 1. Read First Packet (Loop to handle Keepalives) loop { + if start_time.elapsed() > timeout_duration { + log::error!("Timeout waiting for device response (Keepalive limit exceeded)"); + return Err(PFError::Device( + "Timeout waiting for device response (Keepalive limit exceeded)".into(), + )); + } + if let Err(e) = self .device .read_timeout(&mut buf[..], HID_RESP_READ_TIMEOUT_MS) @@ -351,10 +383,7 @@ impl HidTransport { // 0x01: vendorCommandId (u64) // 0x02/0x03/0x04: param let mut sub_params_inner = BTreeMap::new(); - sub_params_inner.insert( - Value::Integer(0x01), - Value::Integer(vendor_cmd.to_u64() as i128), - ); + sub_params_inner.insert(Value::Integer(0x01), Value::Integer(vendor_cmd as i128)); match param { Value::Bytes(_) => { @@ -405,7 +434,7 @@ impl HidTransport { let mut payload = vec![CtapCommand::Config as u8]; payload.extend(config_payload_cbor); - // Send via HID + log::debug!("Sending config command..."); self.send_cbor(CTAPHID_CBOR, &payload).map_err(|e| { log::error!("Failed to send FIDO config: {}", e); PFError::Device(format!("FIDO config failed: {}", e)) @@ -473,7 +502,7 @@ impl HidTransport { let mut payload = vec![CtapCommand::Config as u8]; payload.extend(config_payload_cbor); - // Send via HID + log::debug!("Sending minimum PIN length config command..."); match self.send_cbor(CTAPHID_CBOR, &payload) { Ok(_) => { log::info!( @@ -498,6 +527,538 @@ impl HidTransport { } } + pub fn get_key_agreement(&self) -> Result { + let mut map = BTreeMap::new(); + map.insert( + Value::Integer(ClientPinParam::PinUvAuthProtocol as i128), + Value::Integer(1), + ); + map.insert( + Value::Integer(ClientPinParam::SubCommand as i128), + Value::Integer(ClientPinSubCommand::GetKeyAgreement as i128), + ); + + let mut payload = vec![CtapCommand::ClientPin as u8]; + payload.extend(to_vec(&Value::Map(map)).map_err(|e| PFError::Io(e.to_string()))?); + + log::debug!("Sending GetKeyAgreement command..."); + let resp = self.send_cbor(CTAPHID_CBOR, &payload)?; + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + + if let Value::Map(m) = val { + log::debug!("GetKeyAgreement response: {:?}", m); + m.get(&Value::Integer( + ClientPinResponseParam::KeyAgreement as i128, + )) + .cloned() + .ok_or_else(|| PFError::Device("KeyAgreement not found in response".into())) + } else { + Err(PFError::Device( + "Unexpected response for GetKeyAgreement".into(), + )) + } + } + + pub fn get_pin_token(&self, pin: &str) -> Result, PFError> { + log::info!("Starting custom get_pin_token (Subcommand 0x05)..."); + + // 1. Get Authenticator Key Agreement + let auth_key_agreement = self.get_key_agreement()?; + + // 2. Generate Platform Key Pair (P-256) + let rng = ring::rand::SystemRandom::new(); + let platform_private_key = + agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng) + .map_err(|_| PFError::Device("Failed to generate platform ephemeral key".into()))?; + let platform_public_key_bytes = platform_private_key + .compute_public_key() + .map_err(|_| PFError::Device("Failed to compute platform public key".into()))?; + + // 3. Extract Authenticator Public Key (X and Y coordinates) + let (auth_x, auth_y) = if let Value::Map(m) = &auth_key_agreement { + let x = match m.get(&Value::Integer(-2)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement X coordinate".into())), + }; + let y = match m.get(&Value::Integer(-3)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement Y coordinate".into())), + }; + (x, y) + } else { + return Err(PFError::Device("Invalid KeyAgreement format".into())); + }; + + let mut auth_pub_key_bytes = vec![0x04]; + auth_pub_key_bytes.extend(auth_x); + auth_pub_key_bytes.extend(auth_y); + + let auth_unparsed_pub_key = + agreement::UnparsedPublicKey::new(&agreement::ECDH_P256, auth_pub_key_bytes); + + // 4. Perform ECDH to get Shared Secret + let shared_secret = + agreement::agree_ephemeral(platform_private_key, &auth_unparsed_pub_key, |material| { + let mut hasher = digest::Context::new(&digest::SHA256); + hasher.update(material); + Ok(hasher.finish()) as Result + }) + .map_err(|_| PFError::Device("ECDH shared secret computation failed".into()))? + .map_err(|_| PFError::Device("Inner ECDH shared secret computation failed".into()))?; + + // 5. Encrypt PIN Hash + let pin_hash = digest::digest(&digest::SHA256, pin.as_bytes()); + let pin_hash_16 = &pin_hash.as_ref()[0..16]; + + let iv = [0u8; 16]; + let mut block = *GenericArray::from_slice(pin_hash_16); + + let shared_secret_bytes = shared_secret.as_ref(); + let mut encryptor = cbc::Encryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ); + encryptor.encrypt_block_mut(&mut block); + let pin_hash_enc = block.to_vec(); + + // 6. Send getPinToken command (Subcommand 0x05) + + // 7. Send getPinToken command (Subcommand 0x05) + let cose_key_bytes = self.encode_cose_key( + &platform_public_key_bytes.as_ref()[1..33], + &platform_public_key_bytes.as_ref()[33..65], + ); + + let payload_cbor = self.encode_client_pin_params( + ClientPinSubCommand::GetPinToken, + &cose_key_bytes, + &pin_hash_enc, + None, + None, + ); + + let mut payload = vec![CtapCommand::ClientPin as u8]; + payload.extend(payload_cbor); + + log::debug!("Sending getPinToken command..."); + let resp = self.send_cbor(CTAPHID_CBOR, &payload)?; + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + + if let Value::Map(m) = val { + log::debug!("getPinToken response: {:?}", m); + match m.get(&Value::Integer(ClientPinResponseParam::PinToken as i128)) { + Some(Value::Bytes(token_enc)) => { + // Decrypt the PIN token using shared secret (AES-256-CBC, IV=0) + let mut token_buf = token_enc.clone(); + let decrypted = cbc::Decryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ) + .decrypt_padded_mut::(&mut token_buf) + .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; + log::info!("Successfully obtained and decrypted PIN token (Subcommand 0x05)."); + Ok(decrypted.to_vec()) + } + _ => Err(PFError::Device("pinToken not found in response".into())), + } + } else { + Err(PFError::Device("Unexpected response format".into())) + } + } + + pub fn get_pin_token_with_permission( + &self, + pin: &str, + permissions: PinUvAuthTokenPermissions, + rp_id: Option, + ) -> Result, PFError> { + log::info!( + "Starting custom get_pin_token_with_permission (Subcommand 0x09, permissions: {:?})...", + permissions + ); + + // 1. Get Authenticator Key Agreement + let auth_key_agreement = self.get_key_agreement()?; + + // 2. Generate Platform Key Pair (P-256) + let rng = ring::rand::SystemRandom::new(); + let platform_private_key = + agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng) + .map_err(|_| PFError::Device("Failed to generate platform ephemeral key".into()))?; + let platform_public_key_bytes = platform_private_key + .compute_public_key() + .map_err(|_| PFError::Device("Failed to compute platform public key".into()))?; + + // 3. Extract Authenticator Public Key (X and Y coordinates) + let (auth_x, auth_y) = if let Value::Map(m) = &auth_key_agreement { + let x = match m.get(&Value::Integer(-2)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement X coordinate".into())), + }; + let y = match m.get(&Value::Integer(-3)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement Y coordinate".into())), + }; + (x, y) + } else { + return Err(PFError::Device("Invalid KeyAgreement format".into())); + }; + + let mut auth_pub_key_bytes = vec![0x04]; + auth_pub_key_bytes.extend(auth_x); + auth_pub_key_bytes.extend(auth_y); + + let auth_unparsed_pub_key = + agreement::UnparsedPublicKey::new(&agreement::ECDH_P256, auth_pub_key_bytes); + + // 4. Perform ECDH to get Shared Secret + let shared_secret = + agreement::agree_ephemeral(platform_private_key, &auth_unparsed_pub_key, |material| { + let mut hasher = digest::Context::new(&digest::SHA256); + hasher.update(material); + Ok(hasher.finish()) as Result + }) + .map_err(|_| PFError::Device("ECDH shared secret computation failed".into()))? + .map_err(|_| PFError::Device("Inner ECDH shared secret computation failed".into()))?; + + // 5. Encrypt PIN Hash + let pin_hash = digest::digest(&digest::SHA256, pin.as_bytes()); + let pin_hash_16 = &pin_hash.as_ref()[0..16]; + + let iv = [0u8; 16]; + let mut block = *GenericArray::from_slice(pin_hash_16); + + let shared_secret_bytes = shared_secret.as_ref(); + let mut encryptor = cbc::Encryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ); + encryptor.encrypt_block_mut(&mut block); + let pin_hash_enc = block.to_vec(); + + // 6. Send getPinUvAuthTokenUsingPinWithPermissions command (Subcommand 0x09) + + // 7. Send getPinUvAuthTokenUsingPinWithPermissions command (Subcommand 0x09) + + let mut payload = vec![CtapCommand::ClientPin as u8]; + let cose_key_bytes = self.encode_cose_key( + &platform_public_key_bytes.as_ref()[1..33], + &platform_public_key_bytes.as_ref()[33..65], + ); + + log::trace!( + "Encrypted PIN hash (first 4 bytes): {:?}", + &pin_hash_enc[..4] + ); + let payload_cbor = self.encode_client_pin_params( + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + &cose_key_bytes, + &pin_hash_enc, + Some(permissions.bits()), + rp_id, + ); + payload.extend(payload_cbor); + + log::debug!("Sending getPinUvAuthTokenUsingPinWithPermissions command..."); + let resp = self.send_cbor(CTAPHID_CBOR, &payload)?; + log::debug!( + "getPinUvAuthTokenUsingPinWithPermissions response: {:?}", + resp + ); + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + + if let Value::Map(m) = val { + log::debug!("getPinUvAuthTokenUsingPinWithPermissions response: {:?}", m); + match m.get(&Value::Integer(ClientPinResponseParam::PinToken as i128)) { + Some(Value::Bytes(token_enc)) => { + // Decrypt the PIN token using shared secret (AES-256-CBC, IV=0) + let mut token_buf = token_enc.clone(); + let decrypted = cbc::Decryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ) + .decrypt_padded_mut::(&mut token_buf) + .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; + log::info!("Successfully obtained and decrypted PIN token (Subcommand 0x09)."); + Ok(decrypted.to_vec()) + } + _ => Err(PFError::Device( + "pinUvAuthToken not found in response".into(), + )), + } + } else { + Err(PFError::Device("Unexpected response format".into())) + } + } + + pub fn set_pin(&self, new_pin: &str) -> Result<(), PFError> { + log::info!("Starting custom set_pin (Subcommand 0x03)..."); + + if new_pin.len() < 4 { + return Err(PFError::Device("PIN must be at least 4 characters".into())); + } + if new_pin.len() > 63 { + return Err(PFError::Device( + "PIN must be less than 64 characters".into(), + )); + } + + // 1. Get Authenticator Key Agreement + let auth_key_agreement = self.get_key_agreement()?; + + // 2. Generate Platform Key Pair (P-256) + let rng = ring::rand::SystemRandom::new(); + let platform_private_key = + agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng) + .map_err(|_| PFError::Device("Failed to generate platform ephemeral key".into()))?; + let platform_public_key_bytes = platform_private_key + .compute_public_key() + .map_err(|_| PFError::Device("Failed to compute platform public key".into()))?; + + // 3. Extract Authenticator Public Key + let (auth_x, auth_y) = if let Value::Map(m) = &auth_key_agreement { + let x = match m.get(&Value::Integer(-2)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement X coordinate".into())), + }; + let y = match m.get(&Value::Integer(-3)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement Y coordinate".into())), + }; + (x, y) + } else { + return Err(PFError::Device("Invalid KeyAgreement format".into())); + }; + + let mut auth_pub_key_bytes = vec![0x04]; + auth_pub_key_bytes.extend(auth_x); + auth_pub_key_bytes.extend(auth_y); + + let auth_unparsed_pub_key = + agreement::UnparsedPublicKey::new(&agreement::ECDH_P256, auth_pub_key_bytes); + + // 4. Perform ECDH to get Shared Secret + let shared_secret = + agreement::agree_ephemeral(platform_private_key, &auth_unparsed_pub_key, |material| { + let mut hasher = digest::Context::new(&digest::SHA256); + hasher.update(material); + Ok(hasher.finish()) as Result + }) + .map_err(|_| PFError::Device("ECDH shared secret computation failed".into()))? + .map_err(|_| PFError::Device("Inner ECDH shared secret computation failed".into()))?; + + let shared_secret_bytes = shared_secret.as_ref(); + + // 5. Encrypt newPinEnc + let mut padded_new_pin = [0u8; 64]; + let bytes = new_pin.as_bytes(); + padded_new_pin[..bytes.len()].copy_from_slice(bytes); + + let iv = [0u8; 16]; + let mut new_pin_enc = Vec::new(); + let mut encryptor = cbc::Encryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ); + for chunk in padded_new_pin.chunks_exact(16) { + let mut block = *GenericArray::from_slice(chunk); + encryptor.encrypt_block_mut(&mut block); + new_pin_enc.extend_from_slice(&block); + } + + // 6. Calculate pinUvAuthParam: HMAC-SHA-256(shared_secret, newPinEnc)[0..16] + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, shared_secret_bytes); + let pin_uv_auth_param = hmac::sign(&hmac_key, &new_pin_enc).as_ref()[0..16].to_vec(); + + // 7. Send SetPin command + let cose_key_bytes = self.encode_cose_key( + &platform_public_key_bytes.as_ref()[1..33], + &platform_public_key_bytes.as_ref()[33..65], + ); + + let mut payload_cbor = vec![0xA5]; // Map(5) + payload_cbor + .extend(to_vec(&Value::Integer(ClientPinParam::PinUvAuthProtocol as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(1)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::SubCommand as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinSubCommand::SetPin as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::KeyAgreement as i128)).unwrap()); + payload_cbor.extend(cose_key_bytes); + payload_cbor + .extend(to_vec(&Value::Integer(ClientPinParam::PinUvAuthParam as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Bytes(pin_uv_auth_param)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::NewPinEnc as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Bytes(new_pin_enc)).unwrap()); + + let mut payload = vec![CtapCommand::ClientPin as u8]; + payload.extend(payload_cbor); + + log::debug!("Sending setPin command..."); + match self.send_cbor(CTAPHID_CBOR, &payload) { + Ok(_) => { + log::info!("Successfully set new PIN."); + Ok(()) + } + Err(e) => { + let err_str = e.to_string(); + log::error!("Failed to send setPin config: {}", err_str); + if err_str.contains("0x37") { + return Err(PFError::Device( + "New PIN violates policy (e.g. too short).".into(), + )); + } + Err(PFError::Device(format!("setPin failed: {}", e))) + } + } + } + + pub fn change_pin(&self, current_pin: &str, new_pin: &str) -> Result<(), PFError> { + log::info!("Starting custom change_pin (Subcommand 0x04)..."); + + if new_pin.len() < 4 { + return Err(PFError::Device("PIN must be at least 4 characters".into())); + } + if new_pin.len() > 63 { + return Err(PFError::Device( + "PIN must be less than 64 characters".into(), + )); + } + + // 1. Get Authenticator Key Agreement + let auth_key_agreement = self.get_key_agreement()?; + + // 2. Generate Platform Key Pair (P-256) + let rng = ring::rand::SystemRandom::new(); + let platform_private_key = + agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng) + .map_err(|_| PFError::Device("Failed to generate platform ephemeral key".into()))?; + let platform_public_key_bytes = platform_private_key + .compute_public_key() + .map_err(|_| PFError::Device("Failed to compute platform public key".into()))?; + + // 3. Extract Authenticator Public Key + let (auth_x, auth_y) = if let Value::Map(m) = &auth_key_agreement { + let x = match m.get(&Value::Integer(-2)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement X coordinate".into())), + }; + let y = match m.get(&Value::Integer(-3)) { + Some(Value::Bytes(b)) => b, + _ => return Err(PFError::Device("Invalid KeyAgreement Y coordinate".into())), + }; + (x, y) + } else { + return Err(PFError::Device("Invalid KeyAgreement format".into())); + }; + + let mut auth_pub_key_bytes = vec![0x04]; + auth_pub_key_bytes.extend(auth_x); + auth_pub_key_bytes.extend(auth_y); + + let auth_unparsed_pub_key = + agreement::UnparsedPublicKey::new(&agreement::ECDH_P256, auth_pub_key_bytes); + + // 4. Perform ECDH to get Shared Secret + let shared_secret = + agreement::agree_ephemeral(platform_private_key, &auth_unparsed_pub_key, |material| { + let mut hasher = digest::Context::new(&digest::SHA256); + hasher.update(material); + Ok(hasher.finish()) as Result + }) + .map_err(|_| PFError::Device("ECDH shared secret computation failed".into()))? + .map_err(|_| PFError::Device("Inner ECDH shared secret computation failed".into()))?; + + let shared_secret_bytes = shared_secret.as_ref(); + + // 5. Encrypt current_pin hash + let pin_hash = digest::digest(&digest::SHA256, current_pin.as_bytes()); + let pin_hash_16 = &pin_hash.as_ref()[0..16]; + let iv = [0u8; 16]; + let mut block = *GenericArray::from_slice(pin_hash_16); + cbc::Encryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ) + .encrypt_block_mut(&mut block); + let pin_hash_enc = block.to_vec(); + + // 6. Encrypt newPinEnc + let mut padded_new_pin = [0u8; 64]; + let bytes = new_pin.as_bytes(); + padded_new_pin[..bytes.len()].copy_from_slice(bytes); + + let mut new_pin_enc = Vec::new(); + let mut encryptor = cbc::Encryptor::::new( + GenericArray::from_slice(shared_secret_bytes), + GenericArray::from_slice(&iv), + ); + for chunk in padded_new_pin.chunks_exact(16) { + let mut block = *GenericArray::from_slice(chunk); + encryptor.encrypt_block_mut(&mut block); + new_pin_enc.extend_from_slice(&block); + } + + // 7. Calculate pinUvAuthParam: HMAC-SHA-256(shared_secret, newPinEnc || pinHashEnc)[0..16] + let mut hmac_msg = Vec::new(); + hmac_msg.extend_from_slice(&new_pin_enc); + hmac_msg.extend_from_slice(&pin_hash_enc); + + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, shared_secret_bytes); + let pin_uv_auth_param = hmac::sign(&hmac_key, &hmac_msg).as_ref()[0..16].to_vec(); + + // 8. Send ChangePin command + let cose_key_bytes = self.encode_cose_key( + &platform_public_key_bytes.as_ref()[1..33], + &platform_public_key_bytes.as_ref()[33..65], + ); + + let mut payload_cbor = vec![0xA6]; // Map(6) + payload_cbor + .extend(to_vec(&Value::Integer(ClientPinParam::PinUvAuthProtocol as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(1)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::SubCommand as i128)).unwrap()); + payload_cbor + .extend(to_vec(&Value::Integer(ClientPinSubCommand::ChangePin as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::KeyAgreement as i128)).unwrap()); + payload_cbor.extend(cose_key_bytes); + payload_cbor + .extend(to_vec(&Value::Integer(ClientPinParam::PinUvAuthParam as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Bytes(pin_uv_auth_param)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::NewPinEnc as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Bytes(new_pin_enc)).unwrap()); + payload_cbor.extend(to_vec(&Value::Integer(ClientPinParam::PinHashEnc as i128)).unwrap()); + payload_cbor.extend(to_vec(&Value::Bytes(pin_hash_enc)).unwrap()); + + let mut payload = vec![CtapCommand::ClientPin as u8]; + payload.extend(payload_cbor); + + log::debug!("Sending changePin command..."); + match self.send_cbor(CTAPHID_CBOR, &payload) { + Ok(_) => { + log::info!("Successfully changed PIN."); + Ok(()) + } + Err(e) => { + let err_str = e.to_string(); + log::error!("Failed to send changePin config: {}", err_str); + if err_str.contains("0x31") { + return Err(PFError::Device("Invalid current PIN (0x31). Please check that you entered the correct PIN.".into())); + } + if err_str.contains("0x32") { + return Err(PFError::Device( + "PIN blocked (0x32). Device reset may be required.".into(), + )); + } + if err_str.contains("0x37") { + return Err(PFError::Device( + "New PIN violates policy (e.g. too short).".into(), + )); + } + Err(PFError::Device(format!("changePin failed: {}", e))) + } + } + } + /// Helper to sign the authenticatorConfig command fn sign_config_command( &self, @@ -513,9 +1074,555 @@ impl HidTransport { message.extend(sub_params_bytes); // Sign using provided PIN token - use ring::hmac; + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token); + let sig = hmac::sign(&hmac_key, &message); + sig.as_ref()[0..16].to_vec() + } + + fn encode_cose_key(&self, x: &[u8], y: &[u8]) -> Vec { + let mut bytes = vec![0xA5]; // Map(5) + bytes.extend(to_vec(&Value::Integer(1)).unwrap()); + bytes.extend(to_vec(&Value::Integer(2)).unwrap()); + bytes.extend(to_vec(&Value::Integer(3)).unwrap()); + bytes.extend(to_vec(&Value::Integer(-7)).unwrap()); + bytes.extend(to_vec(&Value::Integer(-1)).unwrap()); + bytes.extend(to_vec(&Value::Integer(1)).unwrap()); + bytes.extend(to_vec(&Value::Integer(-2)).unwrap()); + bytes.extend(to_vec(&Value::Bytes(x.to_vec())).unwrap()); + bytes.extend(to_vec(&Value::Integer(-3)).unwrap()); + bytes.extend(to_vec(&Value::Bytes(y.to_vec())).unwrap()); + bytes + } + + fn encode_client_pin_params( + &self, + sub_cmd: ClientPinSubCommand, + cose_key_bytes: &[u8], + pin_hash_enc: &[u8], + permissions: Option, + rp_id: Option, + ) -> Vec { + let mut count = 4; + if permissions.is_some() { + count += 1; + } + if rp_id.is_some() { + count += 1; + } + let mut bytes = vec![0xA0 | (count as u8)]; + bytes.extend(to_vec(&Value::Integer(ClientPinParam::PinUvAuthProtocol as i128)).unwrap()); + bytes.extend(to_vec(&Value::Integer(1)).unwrap()); + bytes.extend(to_vec(&Value::Integer(ClientPinParam::SubCommand as i128)).unwrap()); + bytes.extend(to_vec(&Value::Integer(sub_cmd as i128)).unwrap()); + bytes.extend(to_vec(&Value::Integer(ClientPinParam::KeyAgreement as i128)).unwrap()); + bytes.extend(cose_key_bytes); + bytes.extend(to_vec(&Value::Integer(ClientPinParam::PinHashEnc as i128)).unwrap()); + bytes.extend(to_vec(&Value::Bytes(pin_hash_enc.to_vec())).unwrap()); + if let Some(p) = permissions { + bytes.extend(to_vec(&Value::Integer(ClientPinParam::Permissions as i128)).unwrap()); + bytes.extend(to_vec(&Value::Integer(p as i128)).unwrap()); + } + if let Some(rp) = rp_id { + bytes.extend(to_vec(&Value::Integer(ClientPinParam::PermissionsRpId as i128)).unwrap()); + bytes.extend(to_vec(&Value::Text(rp)).unwrap()); + } + bytes + } + + pub fn credential_management_enumerate_rps( + &self, + pin: &str, + ) -> Result, PFError> { + log::info!("Starting custom credential_management_enumerate_rps..."); + + // 1. Get PIN token with CREDENTIAL_MANAGEMENT permission + let pin_token = self.get_pin_token_with_permission( + pin, + PinUvAuthTokenPermissions::CREDENTIAL_MANAGEMENT, + None, + )?; + + let mut all_rps = Vec::new(); + + // 2. EnumerateRpsBegin (Subcommand 0x02) + // let sub_params = BTreeMap::new(); + // let sub_params_bytes = to_vec(&Value::Map(sub_params.clone())).unwrap(); + + let pin_auth = self.sign_credential_mgmt_command( + &pin_token, + CredentialMgmtSubCommand::EnumerateRpsBegin as u8, + None, // sub_params_bytes + ); + + let mut mgmt_map = BTreeMap::new(); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommand as i128), + Value::Integer(CredentialMgmtSubCommand::EnumerateRpsBegin as i128), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::PinUvAuthProtocol as i128), + Value::Integer(1), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::PinUvAuthParam as i128), + Value::Bytes(pin_auth), + ); + + let mut payload = vec![CtapCommand::CredentialMgmt as u8]; + payload.extend(to_vec(&Value::Map(mgmt_map)).map_err(|e| PFError::Io(e.to_string()))?); + + let resp = match self.send_cbor(CTAPHID_CBOR, &payload) { + Ok(r) => r, + Err(e) => { + if e.to_string().contains("0x2E") { + log::info!("No credentials found on device (0x2E)"); + return Ok(Vec::new()); + } + return Err(e); + } + }; + + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + let mut total_rps = None; + + if let Value::Map(m) = &val { + let rp = m + .get(&Value::Integer(CredentialMgmtResponseParam::Rp as i128)) + .cloned() + .ok_or_else(|| { + PFError::Device("RP not found in EnumerateRpsBegin response".into()) + })?; + let rp_id_hash = match m.get(&Value::Integer( + CredentialMgmtResponseParam::RpIdHash as i128, + )) { + Some(Value::Bytes(b)) => b.clone(), + _ => { + return Err(PFError::Device( + "RpIdHash not found in EnumerateRpsBegin response".into(), + )); + } + }; + if let Some(Value::Integer(t)) = m.get(&Value::Integer( + CredentialMgmtResponseParam::TotalRps as i128, + )) { + total_rps = Some(*t as usize); + } + + all_rps.push(EnumerateRpResponse { + rp, + rp_id_hash, + total_rps, + }); + } + + // 3. EnumerateRpsGetNextRp (Subcommand 0x03) + let num_to_fetch = total_rps.unwrap_or(1); + while all_rps.len() < num_to_fetch { + let mut mgmt_map = BTreeMap::new(); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommand as i128), + Value::Integer(CredentialMgmtSubCommand::EnumerateRpsGetNextRp as i128), + ); + + let mut payload = vec![CtapCommand::CredentialMgmt as u8]; + payload.extend(to_vec(&Value::Map(mgmt_map)).map_err(|e| PFError::Io(e.to_string()))?); + + match self.send_cbor(CTAPHID_CBOR, &payload) { + Ok(resp) => { + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + if let Value::Map(m) = val { + let rp = m + .get(&Value::Integer(CredentialMgmtResponseParam::Rp as i128)) + .cloned() + .ok_or_else(|| { + PFError::Device( + "RP not found in EnumerateRpsGetNextRp response".into(), + ) + })?; + let rp_id_hash = match m.get(&Value::Integer( + CredentialMgmtResponseParam::RpIdHash as i128, + )) { + Some(Value::Bytes(b)) => b.clone(), + _ => { + return Err(PFError::Device( + "RpIdHash not found in EnumerateRpsGetNextRp response".into(), + )); + } + }; + all_rps.push(EnumerateRpResponse { + rp, + rp_id_hash, + total_rps, + }); + } + } + Err(e) => { + if e.to_string().contains("0x2E") { + break; + } + return Err(e); + } + } + } + + Ok(all_rps) + } + + pub fn credential_management_enumerate_credentials( + &self, + pin: &str, + rp_id_hash: &[u8], + ) -> Result, PFError> { + log::info!("Starting custom credential_management_enumerate_credentials..."); + + // 1. Get PIN token with CREDENTIAL_MANAGEMENT permission + let pin_token = self.get_pin_token_with_permission( + pin, + PinUvAuthTokenPermissions::CREDENTIAL_MANAGEMENT, + None, + )?; + + let mut all_creds = Vec::new(); + + // 2. EnumerateCredentialsBegin (Subcommand 0x04) + let mut sub_params = BTreeMap::new(); + sub_params.insert( + Value::Integer(0x01), // rpIdHash + Value::Bytes(rp_id_hash.to_vec()), + ); + let sub_params_bytes = to_vec(&Value::Map(sub_params.clone())).unwrap(); + + let pin_auth = self.sign_credential_mgmt_command( + &pin_token, + CredentialMgmtSubCommand::EnumerateCredentialsBegin as u8, + Some(&sub_params_bytes), + ); + + let mut mgmt_map = BTreeMap::new(); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommand as i128), + Value::Integer(CredentialMgmtSubCommand::EnumerateCredentialsBegin as i128), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommandParams as i128), + Value::Map(sub_params), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::PinUvAuthProtocol as i128), + Value::Integer(1), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::PinUvAuthParam as i128), + Value::Bytes(pin_auth), + ); + + let mut payload = vec![CtapCommand::CredentialMgmt as u8]; + payload.extend(to_vec(&Value::Map(mgmt_map)).map_err(|e| PFError::Io(e.to_string()))?); + + let resp = match self.send_cbor(CTAPHID_CBOR, &payload) { + Ok(r) => r, + Err(e) => { + if e.to_string().contains("0x2E") { + return Ok(Vec::new()); + } + return Err(e); + } + }; + + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + let mut total_creds = None; + + if let Value::Map(m) = &val { + let user = m + .get(&Value::Integer(CredentialMgmtResponseParam::User as i128)) + .cloned() + .ok_or_else(|| { + PFError::Device("User not found in EnumerateCredentialsBegin response".into()) + })?; + let credential_id = m + .get(&Value::Integer( + CredentialMgmtResponseParam::CredentialId as i128, + )) + .cloned() + .ok_or_else(|| { + PFError::Device( + "CredentialId not found in EnumerateCredentialsBegin response".into(), + ) + })?; + let public_key = m + .get(&Value::Integer( + CredentialMgmtResponseParam::PublicKey as i128, + )) + .cloned() + .ok_or_else(|| { + PFError::Device( + "PublicKey not found in EnumerateCredentialsBegin response".into(), + ) + })?; + if let Some(Value::Integer(t)) = m.get(&Value::Integer( + CredentialMgmtResponseParam::TotalCredentials as i128, + )) { + total_creds = Some(*t as usize); + } + + all_creds.push(EnumerateCredentialResponse { + user, + credential_id, + public_key, + total_credentials: total_creds, + }); + } + + // 3. EnumerateCredentialsGetNextCredential (Subcommand 0x05) + let num_to_fetch = total_creds.unwrap_or(1); + while all_creds.len() < num_to_fetch { + let mut mgmt_map = BTreeMap::new(); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommand as i128), + Value::Integer( + CredentialMgmtSubCommand::EnumerateCredentialsGetNextCredential as i128, + ), + ); + + let mut payload = vec![CtapCommand::CredentialMgmt as u8]; + payload.extend(to_vec(&Value::Map(mgmt_map)).map_err(|e| PFError::Io(e.to_string()))?); + + match self.send_cbor(CTAPHID_CBOR, &payload) { + Ok(resp) => { + let val: Value = from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))?; + if let Value::Map(m) = val { + let user = m + .get(&Value::Integer(CredentialMgmtResponseParam::User as i128)) + .cloned() + .ok_or_else(|| { + PFError::Device( + "User not found in EnumerateCredentialsGetNextCredential response" + .into(), + ) + })?; + let credential_id = m + .get(&Value::Integer(CredentialMgmtResponseParam::CredentialId as i128)) + .cloned() + .ok_or_else(|| { + PFError::Device( + "CredentialId not found in EnumerateCredentialsGetNextCredential response" + .into(), + ) + })?; + let public_key = m + .get(&Value::Integer(CredentialMgmtResponseParam::PublicKey as i128)) + .cloned() + .ok_or_else(|| { + PFError::Device( + "PublicKey not found in EnumerateCredentialsGetNextCredential response" + .into(), + ) + })?; + + all_creds.push(EnumerateCredentialResponse { + user, + credential_id, + public_key, + total_credentials: total_creds, + }); + } + } + Err(e) => { + if e.to_string().contains("0x2E") { + break; + } + return Err(e); + } + } + } + + Ok(all_creds) + } + + pub fn credential_management_delete_credential( + &self, + pin: &str, + credential_id_map: Value, + ) -> Result<(), PFError> { + log::info!("Starting custom credential_management_delete_credential..."); + + // 1. Get PIN token with CREDENTIAL_MANAGEMENT permission + let pin_token = self.get_pin_token_with_permission( + pin, + PinUvAuthTokenPermissions::CREDENTIAL_MANAGEMENT, + None, + )?; + + // 2. DeleteCredential (Subcommand 0x06) + let mut sub_params = BTreeMap::new(); + sub_params.insert( + Value::Integer(0x02), // credentialId descriptor map + credential_id_map, + ); + let sub_params_bytes = to_vec(&Value::Map(sub_params.clone())).unwrap(); + + let pin_auth = self.sign_credential_mgmt_command( + &pin_token, + CredentialMgmtSubCommand::DeleteCredential as u8, + Some(&sub_params_bytes), + ); + + let mut mgmt_map = BTreeMap::new(); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommand as i128), + Value::Integer(CredentialMgmtSubCommand::DeleteCredential as i128), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::SubCommandParams as i128), + Value::Map(sub_params), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::PinUvAuthProtocol as i128), + Value::Integer(1), + ); + mgmt_map.insert( + Value::Integer(CredentialMgmtParam::PinUvAuthParam as i128), + Value::Bytes(pin_auth), + ); + + let mut payload = vec![CtapCommand::CredentialMgmt as u8]; + payload.extend(to_vec(&Value::Map(mgmt_map)).map_err(|e| PFError::Io(e.to_string()))?); + + self.send_cbor(CTAPHID_CBOR, &payload)?; + + Ok(()) + } + + fn sign_credential_mgmt_command( + &self, + pin_token: &[u8], + sub_cmd: u8, + sub_params_bytes: Option<&[u8]>, + ) -> Vec { + // Research into pico-fido firmware reveals a non-standard signing logic: + // 1. No 32-byte 0xff padding. + // 2. No command byte (0x0d). + // 3. For subcommands 0x01 (GetCredsMetadata) and 0x02 (EnumerateRpsBegin), only sign the subcommand byte. + // 4. For others, sign the subcommand byte followed by the CBOR-encoded SubCommandParams map. + + let mut message = vec![sub_cmd]; + if let Some(params) = sub_params_bytes + && sub_cmd != CredentialMgmtSubCommand::GetCredsMetadata as u8 + && sub_cmd != CredentialMgmtSubCommand::EnumerateRpsBegin as u8 + { + message.extend(params); + } + + log::debug!( + "Custom CredentialMgmt signing for sub_cmd 0x{:02x}, message len: {}", + sub_cmd, + message.len() + ); + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token); let sig = hmac::sign(&hmac_key, &message); sig.as_ref()[0..16].to_vec() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_pin_command_ordering() { + // This test doesn't run HID IO, but verifies that our BTreeMap usage + // (which is used in get_pin_token and get_pin_token_with_permission) + // results in correct CBOR key ordering. + let mut map = BTreeMap::new(); + map.insert(Value::Integer(0x01), Value::Integer(1)); // pinProtocol + map.insert(Value::Integer(0x02), Value::Integer(8)); // subCommand (getPinUvAuthToken...) + map.insert(Value::Integer(0x03), Value::Map(BTreeMap::new())); // keyAgreement + map.insert(Value::Integer(0x04), Value::Bytes(vec![0u8; 16])); // pinHashEnc + map.insert(Value::Integer(0x09), Value::Integer(0x01)); // permissions + + let cbor = to_vec(&Value::Map(map)).unwrap(); + + // Canonical CBOR requires keys to be in ascending order. + // BTreeMap in Rust is already ordered by key. + // So 0x01, 0x02, 0x03, 0x04, 0x09 should be in order. + + // Let's check the first few bytes of the map + // 0xA5 (Map of 5) + // 0x01 (Key 1) ... + assert_eq!(cbor[0], 0xA5); + assert_eq!(cbor[1], 0x01); + // We just care that it's ordered for pico-fido + } + + #[test] + fn test_get_key_agreement_parsing_logic() { + use std::collections::BTreeMap; + // Simulate a response map where key 0x01 is the KeyAgreement (as per CTAP 2.1) + let mut inner_map = BTreeMap::new(); + inner_map.insert(Value::Integer(1), Value::Integer(2)); // kty: EC2 + inner_map.insert(Value::Integer(-1), Value::Integer(1)); // crv: P-256 + inner_map.insert(Value::Integer(-2), Value::Bytes(vec![0xAA; 32])); // x + inner_map.insert(Value::Integer(-3), Value::Bytes(vec![0xBB; 32])); // y + + let mut resp_map = BTreeMap::new(); + resp_map.insert( + Value::Integer(ClientPinResponseParam::KeyAgreement as i128), + Value::Map(inner_map), + ); + + let val = Value::Map(resp_map); + + // This mimics the logic in get_key_agreement + if let Value::Map(m) = val { + let key_agreement = m.get(&Value::Integer( + ClientPinResponseParam::KeyAgreement as i128, + )); + assert!(key_agreement.is_some()); + if let Some(Value::Map(km)) = key_agreement { + assert_eq!( + km.get(&Value::Integer(-2)), + Some(&Value::Bytes(vec![0xAA; 32])) + ); + } else { + panic!("KeyAgreement should be a map"); + } + } else { + panic!("Expected map"); + } + } + + #[test] + fn test_pin_hash_encryption_actually_encrypts() { + // Verify that our AES-CBC encryption actually modifies the data. + // This guards against the previous bug where encrypt_block_mut + // was called on a temporary copy (buffer.into()), discarding the result. + use aes::cipher::generic_array::GenericArray; + use cbc::cipher::{BlockEncryptMut, KeyIvInit}; + use ring::digest; + + let pin = "123456"; + let pin_hash = digest::digest(&digest::SHA256, pin.as_bytes()); + let pin_hash_16 = &pin_hash.as_ref()[0..16]; + + // Use a known key (32 bytes of zeros) and IV (16 bytes of zeros) + let key = [0u8; 32]; + let iv = [0u8; 16]; + + let mut block = *GenericArray::from_slice(pin_hash_16); + let original = block.clone(); + + let mut encryptor = cbc::Encryptor::::new( + GenericArray::from_slice(&key), + GenericArray::from_slice(&iv), + ); + encryptor.encrypt_block_mut(&mut block); + + // The encrypted block MUST differ from the original + assert_ne!( + block.as_slice(), + original.as_slice(), + "Encryption did not modify the block — the old bug is back!" + ); + } +} diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index 71a9e0b..1977dbe 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -9,49 +9,300 @@ use crate::{ error::PFError, }; use constants::*; -use ctap_hid_fido2::{ - Cfg, FidoKeyHidFactory, - fidokey::{FidoKeyHid, pin::Permission}, - public_key_credential_descriptor::PublicKeyCredentialDescriptor, -}; use hid::*; use serde_cbor_2::{Value, from_slice, to_vec}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; -// Fido functions that require pin: ( Uses ctap_hid_fido2 crate) - -fn get_device() -> Result { - let cfg = Cfg::init(); - FidoKeyHidFactory::create(&cfg).map_err(|e| { - format!( - "Could not connect to FIDO device. Is it plugged in? Error: {:?}", - e - ) - }) -} +// Fido functions that require pin: pub(crate) fn get_fido_info() -> Result { - let device = get_device()?; + log::info!("Reading FIDO device info via custom GetInfo..."); - let info = device - .get_info() - .map_err(|e| format!("Error reading device info: {:?}", e))?; + let transport = + HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; - let options_map: HashMap = info.options.into_iter().collect(); + let info_payload = [CtapCommand::GetInfo as u8]; + let info_res = transport + .send_cbor(CTAPHID_CBOR, &info_payload) + .map_err(|e| format!("GetInfo CTAP command failed: {}", e))?; + + let info_val: Value = + from_slice(&info_res).map_err(|e| format!("Failed to parse GetInfo CBOR: {}", e))?; + + let map = match &info_val { + Value::Map(m) => m, + _ => return Err("GetInfo response is not a CBOR map".into()), + }; + + let mut versions = Vec::new(); + let mut extensions = Vec::new(); + let mut aaguid = String::from("Unknown"); + let mut options = std::collections::HashMap::new(); + let mut max_msg_size: i128 = 0; + let mut pin_protocols = Vec::new(); + let mut remaining_discoverable_credentials: Option = None; + let mut min_pin_length: i128 = 0; + let mut firmware_version_raw: i128 = 0; + let mut vendor_config_commands = Vec::new(); + let mut certifications = std::collections::HashMap::new(); + let mut max_credential_count_in_list = None; + let mut max_credential_id_length = None; + let mut algorithms = Vec::new(); + let mut max_serialized_large_blob_array = None; + let mut force_pin_change = None; + let mut max_cred_blob_length = None; + + for (key, val) in map { + let key_num = match key { + Value::Integer(n) => *n, + _ => continue, + }; + + match key_num { + // 0x01: versions (array of strings) + 0x01 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Text(s) = v { + versions.push(s.clone()); + } + } + log::info!("Device versions (0x01): {:?}", versions); + } + } + // 0x02: extensions (array of strings) + 0x02 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Text(s) = v { + extensions.push(s.clone()); + } + } + log::info!("Device extensions (0x02): {:?}", extensions); + } + } + // 0x03: aaguid (byte string) + 0x03 => { + if let Value::Bytes(b) = val { + aaguid = hex::encode_upper(b); + log::info!("Device aaguid (0x03): {}", aaguid); + } + } + // 0x04: options (map of string -> bool) + 0x04 => { + if let Value::Map(opts_map) = val { + for (k, v) in opts_map { + if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { + options.insert(name.clone(), *enabled); + } + } + log::info!("Device options (0x04): {:?}", options); + } + } + // 0x05: maxMsgSize + 0x05 => { + if let Value::Integer(n) = val { + max_msg_size = *n; + log::info!("Device maxMsgSize (0x05): {}", max_msg_size); + } + } + // 0x06: pinUvAuthProtocols (array of unsigned) + 0x06 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Integer(n) = v { + pin_protocols.push(*n as u32); + } + } + log::info!("Device pinUvAuthProtocols (0x06): {:?}", pin_protocols); + } + } + // 0x07: maxCredentialCountInList + 0x07 => { + if let Value::Integer(n) = val { + max_credential_count_in_list = Some(*n); + log::info!( + "Device maxCredentialCountInList (0x07): {}", + max_credential_count_in_list.unwrap() + ); + } + } + // 0x08: maxCredentialIdLength + 0x08 => { + if let Value::Integer(n) = val { + max_credential_id_length = Some(*n); + log::info!( + "Device maxCredentialIdLength (0x08): {}", + max_credential_id_length.unwrap() + ); + } + } + // 0x0A: algorithms + 0x0A => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Map(m) = v + && let Some(Value::Integer(alg_id)) = m.get(&Value::Text("alg".into())) + { + if let Some(alg) = CoseAlgorithm::from_i128(*alg_id) { + algorithms.push(alg.to_string()); + } else { + algorithms.push(format!("Unknown ({})", alg_id)); + } + } + } + log::info!("Device algorithms (0x0A): {:?}", algorithms); + } + } + // 0x0B: maxSerializedLargeBlobArray + 0x0B => { + if let Value::Integer(n) = val { + max_serialized_large_blob_array = Some(*n); + log::info!( + "Device maxSerializedLargeBlobArray (0x0B): {}", + max_serialized_large_blob_array.unwrap() + ); + } + } + // 0x0C: forcePinChange + 0x0C => { + if let Value::Bool(b) = val { + force_pin_change = Some(*b); + log::info!( + "Device forcePinChange (0x0C): {}", + force_pin_change.unwrap() + ); + } + } + // 0x0D: minPINLength + 0x0D => { + if let Value::Integer(n) = val { + min_pin_length = *n; + log::info!("Device minPINLength (0x0D): {}", min_pin_length); + } + } + // 0x0E: firmwareVersion + 0x0E => { + if let Value::Integer(n) = val { + firmware_version_raw = *n; + log::info!("Device firmwareVersion (0x0E): {}", firmware_version_raw); + } + } + // 0x0F: maxCredBlobLength + 0x0F => { + if let Value::Integer(n) = val { + max_cred_blob_length = Some(*n); + log::info!( + "Device maxCredBlobLength (0x0F): {}", + max_cred_blob_length.unwrap() + ); + } + } + // 0x13: vendorPrototypeConfigCommands (array of unsigned integers) + 0x13 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Integer(n) = v { + let cmd_id = *n as u64; + let cmd_name = VendorConfigCommand::from_u64(cmd_id) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| format!("0x{:016X}", cmd_id)); + vendor_config_commands.push(cmd_name); + } + } + log::info!( + "Device supports {} vendor config commands: {:?}", + vendor_config_commands.len(), + vendor_config_commands + ); + } else { + log::info!("Empty vendor config commands list"); + } + } + // 0x14: remainingDiscoverableCredentials + 0x14 => { + if let Value::Integer(n) = val { + remaining_discoverable_credentials = Some(*n); + log::info!( + "Device remainingDiscoverableCredentials (0x14): {}", + remaining_discoverable_credentials.unwrap() + ); + } + } + // 0x15: certifications (map or array of integers) + 0x15 => { + // log::trace!("Device certifications (0x15): {:?}", val); + match val { + Value::Map(cert_map) => { + for (k, v) in cert_map { + if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { + let display_name = FidoCertification::from_str(name) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| name.clone()); + certifications.insert(display_name, *enabled); + } + } + } + Value::Array(cert_arr) => { + for v in cert_arr { + if let Value::Integer(id) = v { + let cert_id = *id as u64; + let name = FidoCertification::from_u64(cert_id) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| format!("0x{:016X}", cert_id)); + certifications.insert(name, true); + } + } + } + _ => { + log::error!("Unexpected type for device certifications: {:?}", val); + } + } + log::info!("Device certifications (0x15): {:?}", certifications); + } + // All other known keys (0x10-0x12, 0x16) - silently skip + 0x10..=0x12 | 0x16 => { + log::trace!("GetInfo key 0x{:02X} skipped", key_num); + } + // Unknown keys + _ => { + log::debug!("GetInfo: unknown key 0x{:02X}: {:?}", key_num, val); + } + } + } + + let firmware_version = format!( + "{}.{}", + (firmware_version_raw >> 8) & 0xFF, + firmware_version_raw & 0xFF + ); + + log::info!( + "FIDO GetInfo parsed: {} versions, {} extensions, AAGUID={}, FW={}", + versions.len(), + extensions.len(), + aaguid, + firmware_version + ); Ok(FidoDeviceInfo { - versions: info.versions, - extensions: info.extensions, - aaguid: hex::encode_upper(info.aaguid), - options: options_map, - max_msg_size: info.max_msg_size, - pin_protocols: info.pin_uv_auth_protocols, - min_pin_length: info.min_pin_length, - firmware_version: format!( - "{}.{}", - (info.firmware_version >> 8) & 0xFF, - info.firmware_version & 0xFF - ), + versions, + extensions, + aaguid, + options, + max_msg_size, + pin_protocols, + remaining_discoverable_credentials, + min_pin_length, + firmware_version, + vendor_config_commands, + certifications, + max_credential_count_in_list, + max_credential_id_length, + algorithms, + max_serialized_large_blob_array, + force_pin_change, + max_cred_blob_length, }) } @@ -59,19 +310,20 @@ pub(crate) fn change_fido_pin( current_pin: Option, new_pin: String, ) -> Result { - let device = get_device()?; + log::info!("Starting change_fido_pin (custom implementation)..."); + + let transport = + HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; match current_pin { Some(old) => { - device + transport .change_pin(&old, &new_pin) - .map_err(|e| format!("Failed to change PIN: {:?}", e))?; + .map_err(|e| e.to_string())?; Ok("PIN Changed Successfully".into()) } - None => { - device - .set_new_pin(&new_pin) - .map_err(|e| format!("Failed to set PIN: {:?}", e))?; + Option::None => { + transport.set_pin(&new_pin).map_err(|e| e.to_string())?; Ok("PIN Set Successfully".into()) } } @@ -83,31 +335,28 @@ pub(crate) fn set_min_pin_length( ) -> Result { log::info!("Starting set_min_pin_length (custom implementation)..."); - // 1. Obtain PIN token using the library handle - let pin_token = { - let device = get_device()?; - - // Obtain a token with AuthenticatorConfiguration permission (CTAP 2.1) - match device.get_pinuv_auth_token_with_permission( - ¤t_pin, - Permission::AuthenticatorConfiguration, - ) { - Ok(token) => { - log::debug!("Successfully obtained PIN token with ACFG permission."); - token.key - } - Err(e) => { - log::error!("Failed to get PIN token with ACFG permission: {:?}", e); - return Err(format!("Failed to obtain PIN token: {:?}", e)); - } - } - // Library handle 'device' is dropped here, closing the HID session. - }; - - // 2. Open custom HidTransport and send command using the token because ctap-hid-fido2 has a bug where it sends CBOR map keys out of order (0x01, 0x03, 0x04, 0x02) instead of the required ascending order (0x01, 0x02, 0x03, 0x04). The pico-fido firmware strictly requires ascending order. + // 1. Open custom HidTransport let transport = HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; + // 2. Obtain PIN token using the custom implementation + let pin_token = transport + .get_pin_token_with_permission( + ¤t_pin, + PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, + None, + ) + .map_err(|e| { + let err_str = e.to_string(); + log::error!("Failed to get PIN token with ACFG permission: {}", err_str); + if err_str.contains("0x2B") { + return "The device does not support FIDO 2.1 advanced configuration (Error 0x2B). Ensure your device firmware is up to date and supports this feature.".to_string(); + } + format!("Failed to obtain PIN token: {}", err_str) + })?; + + // 3. Send command using the token because ctap-hid-fido2 has a bug where it sends CBOR map keys out of order (0x01, 0x03, 0x04, 0x02) instead of the required ascending order (0x01, 0x02, 0x03, 0x04). The pico-fido firmware strictly requires ascending order. + transport .send_config_set_min_pin_length(&pin_token, min_pin_length) .map_err(|e| format!("Failed to set minimum PIN length: {}", e))?; @@ -119,42 +368,73 @@ pub(crate) fn set_min_pin_length( } pub(crate) fn get_credentials(pin: String) -> Result, String> { - let device = get_device()?; + log::info!("Listing FIDO credentials via custom implementation..."); - let rps = match device.credential_management_enumerate_rps(Some(&pin)) { - Ok(rps) => rps, - Err(e) => { - // CTAP2_ERR_NO_CREDENTIALS (0x2E) means no credentials exist - return empty list - let err_str = format!("{:?}", e); - if err_str.contains("0x2E") || err_str.contains("NO_CREDENTIALS") { - log::info!("No credentials stored on device (CTAP2_ERR_NO_CREDENTIALS)"); - return Ok(Vec::new()); - } - return Err(format!("Failed to enumerate Relying Parties: {:?}", e)); - } - }; + let transport = + HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; + + let rps = transport + .credential_management_enumerate_rps(&pin) + .map_err(|e| format!("Failed to enumerate Relying Parties: {}", e))?; let mut all_credentials = Vec::new(); - for rp in rps { - let creds = device - .credential_management_enumerate_credentials(Some(&pin), &rp.rpid_hash) - .map_err(|e| { - format!( - "Failed to enumerate credentials for RP {}: {:?}", - rp.public_key_credential_rp_entity.id, e - ) - })?; + for rp_res in rps { + let rp_id = if let Value::Map(m) = &rp_res.rp { + match m.get(&Value::Text("id".into())) { + Some(Value::Text(s)) => s.clone(), + _ => "Unknown".to_string(), + } + } else { + "Unknown".to_string() + }; + + let rp_name = if let Value::Map(m) = &rp_res.rp { + match m.get(&Value::Text("name".into())) { + Some(Value::Text(s)) => s.clone(), + _ => rp_id.clone(), + } + } else { + rp_id.clone() + }; + + log::debug!("Enumerating credentials for RP: {}", rp_id); + + let creds = transport + .credential_management_enumerate_credentials(&pin, &rp_res.rp_id_hash) + .map_err(|e| format!("Failed to enumerate credentials for RP {}: {}", rp_id, e))?; for cred in creds { - all_credentials.push(StoredCredential { - credential_id: hex::encode(&cred.public_key_credential_descriptor.id), - rp_id: rp.public_key_credential_rp_entity.id.clone(), - rp_name: rp.public_key_credential_rp_entity.name.clone(), - user_name: cred.public_key_credential_user_entity.name.clone(), - user_display_name: cred.public_key_credential_user_entity.display_name.clone(), - user_id: hex::encode(&cred.public_key_credential_user_entity.id).clone(), - }); + let mut stored_cred = StoredCredential { + credential_id: "".to_string(), + rp_id: rp_id.clone(), + rp_name: rp_name.clone(), + user_name: "".to_string(), + user_display_name: "".to_string(), + user_id: "".to_string(), + }; + + // Parse User Map + if let Value::Map(m) = &cred.user { + if let Some(Value::Text(s)) = m.get(&Value::Text("name".into())) { + stored_cred.user_name = s.clone(); + } + if let Some(Value::Text(s)) = m.get(&Value::Text("displayName".into())) { + stored_cred.user_display_name = s.clone(); + } + if let Some(Value::Bytes(b)) = m.get(&Value::Text("id".into())) { + stored_cred.user_id = hex::encode(b); + } + } + + // Parse Credential ID Descriptor + if let Value::Map(m) = &cred.credential_id + && let Some(Value::Bytes(b)) = m.get(&Value::Text("id".into())) + { + stored_cred.credential_id = hex::encode(b); + } + + all_credentials.push(stored_cred); } } @@ -162,19 +442,22 @@ pub(crate) fn get_credentials(pin: String) -> Result, Stri } pub(crate) fn delete_credential(pin: String, credential_id_hex: String) -> Result { - let device = get_device()?; + log::info!("Deleting FIDO credential via custom implementation..."); + + let transport = + HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; let cred_id_bytes = hex::decode(&credential_id_hex) .map_err(|_| "Invalid Credential ID Hex string".to_string())?; - let descriptor = PublicKeyCredentialDescriptor { - ctype: "public-key".to_string(), - id: cred_id_bytes, - }; + // Create PublicKeyCredentialDescriptor map: { "type": "public-key", "id": } + let mut descriptor = BTreeMap::new(); + descriptor.insert(Value::Text("type".into()), Value::Text("public-key".into())); + descriptor.insert(Value::Text("id".into()), Value::Bytes(cred_id_bytes)); - device - .credential_management_delete_credential(Some(&pin), descriptor) - .map_err(|e| format!("Failed to delete credential: {:?}", e))?; + transport + .credential_management_delete_credential(&pin, Value::Map(descriptor)) + .map_err(|e| format!("Failed to delete credential: {}", e))?; Ok("Credential deleted successfully".into()) } @@ -201,12 +484,16 @@ pub fn read_device_details() -> Result { fw_version ); - let (used, total) = read_memory_stats(&transport)?; - log::debug!( - "Memory Stats: Used={}KB, Total={}KB", - used / 1024, - total / 1024 - ); + let mem_stats = read_memory_stats(&transport)?; + if let Some((used, total)) = mem_stats { + log::debug!( + "Memory Stats: Used={}KB, Total={}KB", + used / 1024, + total / 1024 + ); + } else { + log::info!("Memory Stats: Not Available"); + } let config = read_physical_config(&transport)?; @@ -215,8 +502,8 @@ pub fn read_device_details() -> Result { Ok(FullDeviceStatus { info: DeviceInfo { serial: "?".to_string(), // Serial number is not available through fido - flash_used: used / 1024, - flash_total: total / 1024, + flash_used: mem_stats.map(|(u, _)| u / 1024), + flash_total: mem_stats.map(|(_, t)| t / 1024), firmware_version: fw_version, }, config, @@ -230,7 +517,7 @@ fn read_device_info(transport: &HidTransport) -> Result<(String, String), PFErro log::debug!("Sending GetInfo command (0x04)..."); let info_payload = [CtapCommand::GetInfo as u8]; let info_res = transport - .send_cbor(CTAPHID_CBOR, &info_payload) + .send_cbor(CTAPHID_CBOR, &info_payload[..]) .map_err(|e| { log::error!("GetInfo CTAP command failed: {}", e); PFError::Device(format!("GetInfo failed: {}", e)) @@ -281,7 +568,7 @@ fn read_device_info(transport: &HidTransport) -> Result<(String, String), PFErro Ok((aaguid_str, fw_version)) } -fn read_memory_stats(transport: &HidTransport) -> Result<(u32, u32), PFError> { +fn read_memory_stats(transport: &HidTransport) -> Result, PFError> { log::debug!("Preparing Memory Stats vendor command..."); let mut mem_req = BTreeMap::new(); @@ -302,9 +589,20 @@ fn read_memory_stats(transport: &HidTransport) -> Result<(u32, u32), PFError> { let mem_res = transport .send_cbor(CTAP_VENDOR_CBOR_CMD, &mem_payload) .map_err(|e| { + // Error code 0x2B means the feature is not supported/removed in this firmware mode + if e.to_string().contains("0x2B") { + log::info!("Memory stats not supported by device firmware (0x2B)."); + return PFError::NoDevice; // We'll handle this specially + } log::warn!("Failed to fetch memory stats (Vendor Cmd): {}", e); PFError::Device(format!("Failed to fetch memory stats: {}", e)) - })?; + }); + + let mem_res = match mem_res { + Ok(res) => res, + Err(PFError::NoDevice) => return Ok(None), + Err(e) => return Err(e), + }; let mem_map: BTreeMap = if !mem_res.is_empty() { from_slice(&mem_res).map_err(|e| { @@ -324,7 +622,7 @@ fn read_memory_stats(transport: &HidTransport) -> Result<(u32, u32), PFError> { .cloned() .unwrap_or(0) as u32; - Ok((used, total)) + Ok(Some((used, total))) } fn read_physical_config(transport: &HidTransport) -> Result { @@ -392,17 +690,21 @@ pub fn write_config(config: AppConfigInput, pin: Option) -> Result Result, PFError> { + match transport.get_pin_token_with_permission( + pin_val, + PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, + None, + ) { Ok(token) => { log::debug!("Successfully obtained PIN token with ACFG permission."); - token.key + Ok(token) } Err(e) => { log::warn!( @@ -410,22 +712,17 @@ pub fn write_config(config: AppConfigInput, pin: Option) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result Result { Ok(FullDeviceStatus { info: DeviceInfo { serial: serial_str, - flash_used: used / 1024, - flash_total: total / 1024, + flash_used: Some(used / 1024), + flash_total: Some(total / 1024), firmware_version: format!("{}.{}", version_major, version_minor), }, config, diff --git a/src/device/types.rs b/src/device/types.rs index 88c2afd..e14334e 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -10,8 +10,8 @@ struct PForgeState { #[serde(rename_all = "camelCase")] pub struct DeviceInfo { pub serial: String, - pub flash_used: u32, - pub flash_total: u32, + pub flash_used: Option, + pub flash_total: Option, pub firmware_version: String, } @@ -74,11 +74,21 @@ pub struct FidoDeviceInfo { pub extensions: Vec, pub aaguid: String, pub options: std::collections::HashMap, - pub max_msg_size: i32, + pub max_msg_size: i128, pub pin_protocols: Vec, - // pub remaining_disc_creds: u32, - pub min_pin_length: u32, + pub remaining_discoverable_credentials: Option, + pub min_pin_length: i128, pub firmware_version: String, + /// Supported vendor config commands (human-readable names), parsed from CTAP GetInfo key 0x13 + pub vendor_config_commands: Vec, + /// Device certifications, parsed from CTAP GetInfo key 0x15 + pub certifications: std::collections::HashMap, + pub max_credential_count_in_list: Option, + pub max_credential_id_length: Option, + pub algorithms: Vec, + pub max_serialized_large_blob_array: Option, + pub force_pin_change: Option, + pub max_cred_blob_length: Option, } #[derive(Debug, Clone, Serialize)] diff --git a/src/logging.rs b/src/logging.rs index dd85049..9602637 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,9 +1,7 @@ use directories::ProjectDirs; use log::LevelFilter; -use log::Record; use log4rs::{ append::{ - Append, console::{ConsoleAppender, Target}, rolling_file::{ RollingFileAppender, @@ -13,57 +11,9 @@ use log4rs::{ }, }, config::{Appender, Logger, Root}, - encode::{Encode, Write, pattern::PatternEncoder}, + encode::pattern::PatternEncoder, }; use std::fs; -use std::sync::{Arc, Mutex, OnceLock}; - -pub static LOG_BUFFER: OnceLock>>> = OnceLock::new(); - -struct SimpleWriter(Vec); - -impl std::io::Write for SimpleWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.0.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -impl Write for SimpleWriter {} - -#[derive(Debug)] -struct BufferAppender { - encoder: Box, -} - -impl BufferAppender { - pub fn new(encoder: Box) -> Self { - Self { encoder } - } -} - -impl Append for BufferAppender { - fn append(&self, record: &Record) -> anyhow::Result<()> { - let mut writer = SimpleWriter(Vec::new()); - self.encoder.encode(&mut writer, record)?; - let log = String::from_utf8(writer.0)?; - - let buffer = LOG_BUFFER.get_or_init(|| Arc::new(Mutex::new(Vec::new()))); - if let Ok(mut logs) = buffer.lock() { - if logs.len() >= 1000 { - logs.remove(0); - } - logs.push(log); - } - Ok(()) - } - - fn flush(&self) {} -} /// Initializes log4rs with custom configuration for stdout and file logging. pub fn logger_init() { @@ -71,8 +21,7 @@ pub fn logger_init() { let org = "suyogtandel"; let app = "picoforge"; - LOG_BUFFER.get_or_init(|| Arc::new(Mutex::new(Vec::new()))); - + // Determine the log file path using ProjectDirs for cross-platform compatibility let log_file_path = { let log_dir = if let Some(proj_dirs) = ProjectDirs::from(qual, org, app) { proj_dirs.data_local_dir().join("logs") @@ -109,34 +58,20 @@ pub fn logger_init() { ))) .build(); - // Buffer Appender - let buffer_appender = BufferAppender::new(Box::new(PatternEncoder::new( - "[{d(%Y-%m-%d %H:%M:%S %Z)} {l} {t}] {m}{n}", - ))); - - let app_level = if cfg!(debug_assertions) { - LevelFilter::Trace + let (app_level, root_level) = if cfg!(debug_assertions) { + (LevelFilter::Trace, LevelFilter::Debug) } else { - LevelFilter::Info + (LevelFilter::Info, LevelFilter::Error) }; let config = log4rs::Config::builder() .appender(Appender::builder().build("stdout", Box::new(stdout))) .appender(Appender::builder().build("logfile", Box::new(logfile))) - .appender(Appender::builder().build("buffer", Box::new(buffer_appender) as Box)) - .logger( - Logger::builder() - .appenders(["stdout", "logfile", "buffer"]) - .additive(false) - .build("picoforge", app_level), - ) - .logger(Logger::builder().build("gpui", LevelFilter::Error)) - .logger(Logger::builder().build("gpui_component", LevelFilter::Error)) - .logger(Logger::builder().build("blade_graphics", LevelFilter::Error)) + .logger(Logger::builder().build("picoforge", app_level)) .build( Root::builder() - .appenders(vec!["logfile", "stdout", "buffer"]) - .build(LevelFilter::Error), + .appenders(vec!["logfile", "stdout"]) + .build(root_level), ) .unwrap(); diff --git a/src/main.rs b/src/main.rs index fb2a35c..6ffa6d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,7 +63,6 @@ fn main() { titlebar: Some(TitlebarOptions { title: Some("PicoForge".into()), appears_transparent: true, - // TODO: This option needs to be tested and adjusted on macos traffic_light_position: Some(gpui::point(px(9.0), px(9.0))), }), @@ -75,7 +74,7 @@ fn main() { window_min_size: Some(gpui::Size { width: px(450.), - height: px(200.), + height: px(400.), }), kind: WindowKind::Normal, ..Default::default() diff --git a/src/ui/components/button.rs b/src/ui/components/button.rs index 7a5e4ae..6672629 100644 --- a/src/ui/components/button.rs +++ b/src/ui/components/button.rs @@ -10,12 +10,14 @@ use gpui_component::{ h_flex, }; +type ClickHandler = Option>; + /// A stateless text button wrapper #[derive(IntoElement)] pub struct PFButton { id: SharedString, text: SharedString, - on_click: Option>, + on_click: ClickHandler, bg_color_start: Rgba, bg_color_hover: Rgba, bg_color_active: Rgba, @@ -159,7 +161,7 @@ impl RenderOnce for PFButton { pub struct PFIconButton { icon: Icon, text: SharedString, - on_click: Option>, + on_click: ClickHandler, bg_color_start: Rgba, bg_color_hover: Rgba, bg_color_active: Rgba, diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index d29a3d4..ec0aaa3 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -7,6 +7,12 @@ use gpui_component::{ v_flex, }; +type PinPromptCallback = std::rc::Rc, &mut App)>; +type ConfirmCallback = std::rc::Rc, &mut App)>; +type ChangePinCallback = + std::rc::Rc, &mut App)>; +type SetPinCallback = std::rc::Rc, &mut App)>; + #[derive(Clone)] enum DialogPhase { Input, @@ -21,7 +27,7 @@ pub struct PinPromptContent { description: SharedString, confirm_label: SharedString, pin_input: Entity, - on_confirm: std::rc::Rc, &mut App)>, + on_confirm: PinPromptCallback, _subscription: Subscription, } @@ -117,10 +123,10 @@ impl Render for PinPromptContent { .px_3() .py_2() .rounded_md() - .bg(cx.theme().danger.opacity(0.1)) - .text_color(cx.theme().danger) + .bg(rgb(0x18181b)) + .text_color(rgb(0xef4444)) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child(Input::new(&pin_input)) .child( @@ -244,7 +250,7 @@ pub struct ConfirmContent { message: String, ok_label: SharedString, ok_variant: ButtonVariant, - on_ok: std::rc::Rc, &mut App)>, + on_ok: ConfirmCallback, } impl ConfirmContent { @@ -326,10 +332,10 @@ impl Render for ConfirmContent { .px_3() .py_2() .rounded_md() - .bg(cx.theme().danger.opacity(0.1)) - .text_color(cx.theme().danger) + .bg(rgb(0x18181b)) + .text_color(rgb(0xef4444)) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( h_flex() @@ -426,7 +432,7 @@ pub struct ChangePinContent { current_pin: Entity, new_pin: Entity, confirm_pin: Entity, - on_confirm: std::rc::Rc, &mut App)>, + on_confirm: ChangePinCallback, _subscriptions: Vec, } @@ -548,10 +554,10 @@ impl Render for ChangePinContent { .px_3() .py_2() .rounded_md() - .bg(cx.theme().danger.opacity(0.1)) - .text_color(cx.theme().danger) + .bg(rgb(0x18181b)) + .text_color(rgb(0xef4444)) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( v_flex() @@ -742,7 +748,7 @@ pub struct SetPinContent { phase: DialogPhase, new_pin: Entity, confirm_pin: Entity, - on_confirm: std::rc::Rc, &mut App)>, + on_confirm: SetPinCallback, _subscriptions: Vec, } @@ -856,10 +862,10 @@ impl Render for SetPinContent { .px_3() .py_2() .rounded_md() - .bg(cx.theme().danger.opacity(0.1)) - .text_color(cx.theme().danger) + .bg(rgb(0x18181b)) + .text_color(rgb(0xef4444)) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( v_flex() @@ -1091,10 +1097,10 @@ impl Render for StatusContent { .px_3() .py_2() .rounded_md() - .bg(cx.theme().danger.opacity(0.1)) - .text_color(cx.theme().danger) + .bg(rgb(0x18181b)) + .text_color(rgb(0xef4444)) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( h_flex() @@ -1148,3 +1154,30 @@ pub fn open_status_dialog( handle } + +fn render_error_message(msg: String) -> impl IntoElement { + let troubleshooting_phrase = "troubleshooting guide"; + let url = "https://github.com/librekeys/picoforge/wiki/Troubleshooting#1-my-key-is-not-detected-by-picoforge-or-picoforge-displays-a-device-status-of-online---fido-and-there-are-some-settings-that-i-cannot-configure"; + + if msg.contains(troubleshooting_phrase) { + v_flex() + .child("The device firmware does not support being configured in fido only communication mode.") + .child( + h_flex() + .gap_1() + .child("Have a look at the") + .child( + div() + .text_color(rgb(0x3b82f6)) + .cursor_pointer() + .on_mouse_down(MouseButton::Left, move |_, _, cx| { + cx.open_url(url); + }) + .child(troubleshooting_phrase.to_string()), + ) + .child("to fix this"), + ) + } else { + div().child(msg) + } +} diff --git a/src/ui/components/sidebar.rs b/src/ui/components/sidebar.rs index aa41cd7..35caaca 100644 --- a/src/ui/components/sidebar.rs +++ b/src/ui/components/sidebar.rs @@ -11,13 +11,16 @@ use gpui_component::{ }; use std::rc::Rc; +type SelectHandler = Option)>>; +type RefreshHandler = Option)>>; + pub struct AppSidebar { active_view: ActiveView, width: Pixels, collapsed: bool, state: GlobalDeviceState, - on_select: Option)>>, - on_refresh: Option)>>, + on_select: SelectHandler, + on_refresh: RefreshHandler, } impl AppSidebar { @@ -151,12 +154,6 @@ impl AppSidebar { "icons/shield-check.svg", ActiveView::Security, )) - .child(self.menu_item( - cx, - "Logs", - "icons/scroll-text.svg", - ActiveView::Logs, - )) .child(self.menu_item_icon_name( cx, "About", diff --git a/src/ui/rootview.rs b/src/ui/rootview.rs index cbb6448..498ad1a 100644 --- a/src/ui/rootview.rs +++ b/src/ui/rootview.rs @@ -2,7 +2,7 @@ use crate::device::io; use crate::ui::components::sidebar::AppSidebar; use crate::ui::types::{ActiveView, GlobalDeviceState}; use crate::ui::views::{ - about::AboutView, config::ConfigView, home::HomeView, logs::LogsView, passkeys::PasskeysEvent, + about::AboutView, config::ConfigView, home::HomeView, passkeys::PasskeysEvent, passkeys::PasskeysView, security::SecurityView, }; @@ -25,7 +25,6 @@ pub struct ApplicationRoot { sidebar_width: Pixels, config_view: Option>, passkeys_view: Option>, - logs_view: Option>, focus_handle: FocusHandle, } @@ -40,7 +39,6 @@ impl ApplicationRoot { sidebar_width: px(255.), config_view: None, passkeys_view: None, - logs_view: None, focus_handle: cx.focus_handle(), }; this.refresh_device_status(None, cx); @@ -75,12 +73,12 @@ impl ApplicationRoot { } } - if let Some(config_view) = &self.config_view { - if let Some(window) = window { - config_view.update(cx, |view, cx| { - view.update_device_status(Some(status.clone()), window, cx); - }); - } + if let Some(config_view) = &self.config_view + && let Some(window) = window + { + config_view.update(cx, |view, cx| { + view.update_device_status(Some(status.clone()), window, cx); + }); } if let Some(passkeys_view) = &self.passkeys_view { @@ -125,8 +123,8 @@ impl Render for ApplicationRoot { self.sidebar_width = target_width; } - let dialog_layer = Root::render_dialog_layer(window, &mut **cx); - let sheet_layer = Root::render_sheet_layer(window, &mut **cx); + let dialog_layer = Root::render_dialog_layer(window, cx); + let sheet_layer = Root::render_sheet_layer(window, cx); let title_bar = TitleBar::new().bg(cx.theme().title_bar).child( h_flex() @@ -170,9 +168,6 @@ impl Render for ApplicationRoot { PasskeysEvent::Notification(msg) => { window.push_notification(msg.to_string(), cx); } - PasskeysEvent::CloseDialog => { - window.close_dialog(cx); - } }, ) .detach(); @@ -187,12 +182,6 @@ impl Render for ApplicationRoot { view.clone().into_any_element() } ActiveView::Security => SecurityView::build(cx).into_any_element(), - ActiveView::Logs => { - let view = self - .logs_view - .get_or_insert_with(|| cx.new(|cx| LogsView::new(window, cx))); - view.clone().into_any_element() - } ActiveView::About => AboutView::build(cx.theme()).into_any_element(), }); diff --git a/src/ui/types.rs b/src/ui/types.rs index 5155e2b..f69df55 100644 --- a/src/ui/types.rs +++ b/src/ui/types.rs @@ -8,7 +8,6 @@ pub enum ActiveView { Passkeys, Configuration, Security, - Logs, About, } @@ -35,6 +34,7 @@ impl GlobalDeviceState { pub enum UsbIdentityPreset { Custom, Generic, + LibreKeys, PicoHsm, PicoFido, PicoOpenPgp, @@ -57,6 +57,11 @@ impl UsbIdentityPreset { match self { Self::Custom => ("Custom (Manual Entry)".into(), None, None), Self::Generic => ("Generic (FEFF:FCFD)".into(), Some("FEFF"), Some("FCFD")), + Self::LibreKeys => ( + "LibreKeys One (1D50:619B)".into(), + Some("1D50"), + Some("619B"), + ), Self::PicoHsm => ( "Pico Keys HSM (2E8A:10FD)".into(), Some("2E8A"), @@ -94,6 +99,7 @@ impl UsbIdentityPreset { match (vid.as_str(), pid.as_str()) { ("FEFF", "FCFD") => Self::Generic, + ("1D50", "619B") => Self::LibreKeys, ("2E8A", "10FD") => Self::PicoHsm, ("2E8A", "10FE") => Self::PicoFido, ("2E8A", "10FF") => Self::PicoOpenPgp, @@ -116,6 +122,7 @@ impl UsbIdentityPreset { &[ Self::Custom, Self::Generic, + Self::LibreKeys, Self::PicoHsm, Self::PicoFido, Self::PicoOpenPgp, diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index c12f5df..18b617c 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -228,11 +228,12 @@ impl ConfigView { cx.notify(); let entity = cx.entity().downgrade(); + let method_clone = method.clone(); self._task = Some(cx.spawn(async move |_, cx| { let result = cx .background_executor() - .spawn(async move { io::write_config(changes, method, pin) }) + .spawn(async move { io::write_config(changes, method_clone, pin) }) .await; let new_status_result = if result.is_ok() { @@ -289,15 +290,25 @@ impl ConfigView { } Err(e) => { log::error!("Error saving config: {}", e); + + let mut err_msg = format!("Failed to apply configuration: {}", e); + + // Special case for FIDO 0x3E error (Invalid Subcommand) + // This happens when the firmware is too old to support config over FIDO + if method == crate::device::types::DeviceMethod::Fido && err_msg.contains("0x3E") + { + err_msg = "The device firmware does not support being configured in fido only communication mode. \nHave a look at the troubleshooting guide to fix this".to_string(); + } + match &dialog_handle { StatusDialogHandle::Pin(dh) => { let _ = dh.update(cx, |d, cx| { - d.set_error(format!("Failed to apply: {}", e), cx); + d.set_error(err_msg, cx); }); } StatusDialogHandle::Status(dh) => { let _ = dh.update(cx, |d, cx| { - d.set_error(format!("Failed to apply: {}", e), cx); + d.set_error(err_msg, cx); }); } } @@ -375,19 +386,19 @@ impl ConfigView { } let led_gpio_str = self.led_gpio_input.read(cx).text().to_string(); - if let Ok(val) = led_gpio_str.parse::() { - if val != current_config.led_gpio { - changes.led_gpio = Some(val); - } + if let Ok(val) = led_gpio_str.parse::() + && val != current_config.led_gpio + { + changes.led_gpio = Some(val); } let driver_idx = self.led_driver_select.read(cx).selected_index(cx); - if let Some(idx) = driver_idx { - if let Some(driver) = LedDriverType::all().get(idx.row) { - let val = driver.value(); - if Some(val) != current_config.led_driver { - changes.led_driver = Some(val); - } + if let Some(idx) = driver_idx + && let Some(driver) = LedDriverType::all().get(idx.row) + { + let val = driver.value(); + if Some(val) != current_config.led_driver { + changes.led_driver = Some(val); } } @@ -397,10 +408,10 @@ impl ConfigView { } let touch_timeout_str = self.touch_timeout_input.read(cx).text().to_string(); - if let Ok(val) = touch_timeout_str.parse::() { - if val != current_config.touch_timeout { - changes.touch_timeout = Some(val); - } + if let Ok(val) = touch_timeout_str.parse::() + && val != current_config.touch_timeout + { + changes.touch_timeout = Some(val); } if (self.led_dimmable != current_config.led_dimmable) @@ -778,7 +789,7 @@ impl Render for ConfigView { })), ), ), - &theme, + theme, ) .into_any_element() } diff --git a/src/ui/views/home.rs b/src/ui/views/home.rs index 153c363..513a87a 100644 --- a/src/ui/views/home.rs +++ b/src/ui/views/home.rs @@ -1,6 +1,7 @@ use crate::device::types::DeviceMethod; use crate::ui::components::{card::Card, page_view::PageView, tag::Tag}; use crate::ui::types::GlobalDeviceState; +use gpui::prelude::FluentBuilder; use gpui::*; use gpui_component::StyledExt; use gpui_component::{Icon, IconName, Theme, h_flex, progress::Progress, v_flex}; @@ -86,8 +87,6 @@ impl HomeView { let info = &status.info; let config = &status.config; - let flash_percent = (info.flash_used as f32 / info.flash_total as f32) * 100.0; - Card::new() .title("Device Information") .icon(Icon::default().path("icons/cpu.svg")) @@ -137,12 +136,25 @@ impl HomeView { .text_color(theme.muted_foreground) .child("Flash Memory"), ) - .child(div().text_color(theme.foreground).child(format!( - "{:.0} / {:.0} KB", - info.flash_used, info.flash_total - ))), + .child(div().text_color(theme.foreground).child( + if let (Some(used), Some(total)) = + (info.flash_used, info.flash_total) + { + format!("{:.0} / {:.0} KB", used, total) + } else { + "Not Available".to_string() + }, + )), ) - .child(Progress::new().value(flash_percent)), + .when( + info.flash_used.is_some() && info.flash_total.is_some(), + |this| { + let used = info.flash_used.unwrap(); + let total = info.flash_total.unwrap(); + let flash_percent = (used as f32 / total as f32) * 100.0; + this.child(Progress::new().value(flash_percent)) + }, + ), ), ) } @@ -153,47 +165,108 @@ impl HomeView { .icon(Icon::default().path("icons/shield.svg")) .child(if let Some(fido) = &state.fido_info { v_flex() - .gap_6() + .gap_3() + .text_sm() + // AAGUID .child( - div() - .grid() - .grid_cols(2) - .gap_4() - .child(Self::render_kv( - "FIDO Version", - fido.versions.first().cloned().unwrap_or("N/A".into()), - theme, - false, - )) - .child(Self::render_kv( - "PIN Set", - if fido.options.get("clientPin").copied().unwrap_or(false) { - "Yes" + h_flex() + .justify_between() + .items_center() + .flex_wrap() + .gap_1() + .child(div().text_color(theme.muted_foreground).child("AAGUID")) + .child( + div() + .font_family("Mono") + .text_color(theme.foreground) + .child(fido.aaguid.clone()), + ), + ) + // FIDO Versions + .child( + h_flex() + .justify_between() + .items_center() + .flex_wrap() + .gap_1() + .child( + div() + .text_color(theme.muted_foreground) + .child("FIDO Versions"), + ) + .child(div().text_color(theme.foreground).child( + if fido.versions.is_empty() { + "N/A".to_string() } else { - "No" + fido.versions.join(" · ") }, - theme, - false, - )) - .child(Self::render_kv( - "Min PIN Length", - fido.min_pin_length.to_string(), - theme, - false, - )) - .child(Self::render_kv( - "Resident Keys", - if fido.options.get("rk").copied().unwrap_or(false) { - "Supported" - } else { - "Not Supported" - }, - theme, - false, )), ) .child(div().h_px().bg(theme.border)) - .child(Self::render_kv("AAGUID", fido.aaguid.clone(), theme, true)) + // PIN Set + .child( + h_flex() + .justify_between() + .items_center() + .child(div().text_color(theme.muted_foreground).child("PIN Set")) + .child({ + let pin_set = + fido.options.get("clientPin").copied().unwrap_or(false); + Tag::new(if pin_set { "Set" } else { "Not Set" }).active(pin_set) + }), + ) + // Resident Keys + .child( + h_flex() + .justify_between() + .items_center() + .child( + div() + .text_color(theme.muted_foreground) + .child("Resident Keys"), + ) + .child({ + let rk = fido.options.get("rk").copied().unwrap_or(false); + Tag::new(if rk { "Supported" } else { "Not Supported" }).active(rk) + }), + ) + // Min PIN Length + .child( + h_flex() + .justify_between() + .items_center() + .child( + div() + .text_color(theme.muted_foreground) + .child("Min PIN Length"), + ) + .child( + div() + .font_medium() + .text_color(theme.foreground) + .child(fido.min_pin_length.to_string()), + ), + ) + // Remaining Credentials + .when(fido.remaining_discoverable_credentials.is_some(), |this| { + this.child( + h_flex() + .justify_between() + .items_center() + .child( + div() + .text_color(theme.muted_foreground) + .child("Remaining Credentials"), + ) + .child( + div().font_medium().text_color(theme.foreground).child( + fido.remaining_discoverable_credentials + .unwrap_or(0) + .to_string(), + ), + ), + ) + }) .into_any_element() } else { div() diff --git a/src/ui/views/logs.rs b/src/ui/views/logs.rs deleted file mode 100644 index e0bb153..0000000 --- a/src/ui/views/logs.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::logging::LOG_BUFFER; -use crate::ui::components::page_view::PageView; -use gpui::*; -use gpui_component::{ - ActiveTheme, Icon, button::Button, h_flex, scroll::ScrollableElement, v_flex, -}; - -pub struct LogsView { - logs: Vec, -} - -impl LogsView { - pub fn new(_window: &mut Window, cx: &mut Context) -> Self { - let view_weak = cx.entity().downgrade(); - let mut cx_async = cx.to_async(); - - cx.spawn(async move |_, _| { - loop { - cx_async - .background_executor() - .timer(std::time::Duration::from_millis(250)) - .await; - - if let Some(view) = view_weak.upgrade() { - view.update(&mut cx_async, |view, cx| { - view.sync_logs(); - cx.notify(); - }) - .ok(); - } else { - break; - } - } - }) - .detach(); - - Self { logs: Vec::new() } - } - - fn sync_logs(&mut self) { - if let Some(buffer) = LOG_BUFFER.get() { - if let Ok(logs) = buffer.lock() { - self.logs = logs.clone(); - } - } - } - - fn clear(&mut self, cx: &mut Context) { - if let Some(buffer) = LOG_BUFFER.get() { - if let Ok(mut logs) = buffer.lock() { - logs.clear(); - } - } - self.logs.clear(); - cx.notify(); - } -} - -impl Render for LogsView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let clear_logs_listener = cx.listener(|this, _, _, cx| { - this.clear(cx); - }); - - let copy_logs_listener = cx.listener(|this, _, _, cx| { - let all_logs = this.logs.join("\n"); - log::debug!("Copying {} bytes of logs", all_logs.len()); - cx.write_to_clipboard(ClipboardItem::new_string(all_logs)); - }); - - let theme = cx.theme(); - - PageView::build( - "System Logs", - "Real-time device communication and application events.", - v_flex() - .gap_4() - .h_full() - .child( - div() - .flex_1() - .flex() - .flex_col() - .min_h(px(500.0)) - .bg(gpui::black()) - .border_1() - .border_color(theme.border) - .rounded(theme.radius) - .child(if self.logs.is_empty() { - div() - .flex_1() - .flex() - .flex_col() - .items_center() - .justify_center() - .text_color(theme.muted_foreground) - .child( - Icon::default() - .path("icons/terminal.svg") - .size_12() - .text_color(theme.muted_foreground) - .opacity(0.5), - ) - .child(div().mt_4().child("No events recorded yet.")) - .into_any_element() - } else { - div() - .overflow_y_scrollbar() - .flex_1() - .h(px(500.0)) - .child( - div() - .overflow_y_scrollbar() - .max_h(px(500.0)) - .p_4() - .font_family("Mono") - .text_sm() - .child(v_flex().gap_neg_4().children( - self.logs.iter().map(|log| { - // TODO: Convert these values to constants in colors.rs - let color = if log.contains("ERROR") { - rgb(0xef4444) - } else if log.contains("WARN") { - rgb(0xfde047) - } else if log.contains("INFO") { - rgb(0x4ade80) - } else { - theme.foreground.to_rgb() - }; - - div().text_color(color).child(log.clone()) - }), - )), - ) - .into_any_element() - }), - ) - .child( - h_flex() - .justify_end() - .gap_2() - .child( - // NOTE: This does not work on linux as of now(tested on NIXOS-26_GNOME-49_WAYLAND) - Button::new("copy_logs") - .label("Copy Logs") - .on_click(copy_logs_listener), - ) - .child( - Button::new("clear_logs") - .label("Clear Logs") - .on_click(clear_logs_listener), - ), - ), - theme, - ) - } -} diff --git a/src/ui/views/mod.rs b/src/ui/views/mod.rs index c325063..0326cc7 100644 --- a/src/ui/views/mod.rs +++ b/src/ui/views/mod.rs @@ -1,6 +1,5 @@ pub mod about; pub mod config; pub mod home; -pub mod logs; pub mod passkeys; pub mod security; diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index 934b698..fc5b08c 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -4,7 +4,7 @@ use crate::ui::components::{ button::{PFButton, PFIconButton}, card::Card, dialog, - dialog::{ChangePinContent, ConfirmContent, PinPromptContent, SetPinContent}, + dialog::{ChangePinContent, ConfirmContent, PinPromptContent, SetPinContent, StatusContent}, page_view::PageView, }; use gpui::*; @@ -42,7 +42,6 @@ pub struct PasskeysView { pub enum PasskeysEvent { Notification(String), - CloseDialog, } impl EventEmitter for PasskeysView {} @@ -333,19 +332,64 @@ impl PasskeysView { .masked(true) }); - // Create the label view let label_view = cx.new(|_cx| SliderLabel { slider: slider.clone(), }); let view_handle = cx.entity().downgrade(); - window.open_dialog(cx, move |dialog, _, _| { - let view = view_handle.clone(); + // Shared submit closure used by both the Enter key (on_ok) and the Update button. + let submit = { + let current_pin2 = current_pin.clone(); + let new_pin2 = new_pin.clone(); + let confirm_pin2 = confirm_pin.clone(); + let slider2 = slider.clone(); + let view2 = view_handle.clone(); + std::rc::Rc::new(move |window: &mut Window, cx: &mut App| { + let current_val = current_pin2.read(cx).text().to_string(); + let new_val = new_pin2.read(cx).text().to_string(); + let confirm_val = confirm_pin2.read(cx).text().to_string(); + let min_len = slider2.read(cx).value().start() as u8; + + if current_val.is_empty() { + return; + } + + if !new_val.is_empty() { + if new_val != confirm_val { + let _ = view2.update(cx, |_, cx| { + cx.emit(PasskeysEvent::Notification("PINs do not match".to_string())); + }); + return; + } + if new_val.len() < min_len as usize { + let _ = view2.update(cx, |_, cx| { + cx.emit(PasskeysEvent::Notification(format!( + "PIN must be at least {} characters", + min_len + ))); + }); + return; + } + } + // Close the input dialog and open a status dialog for loading feedback. + window.close_dialog(cx); + let status_handle = + dialog::open_status_dialog("Update Minimum PIN Length", window, cx); + let _ = view2.update(cx, |this, cx| { + this.update_min_length(current_val, min_len, new_val, status_handle, cx); + }); + }) + }; + + window.open_dialog(cx, move |dialog, window, _| { let current = current_pin.clone(); let new = new_pin.clone(); let confirm = confirm_pin.clone(); let slider_handle = slider.clone(); + let submit_for_ok = submit.clone(); + let submit_for_btn = submit.clone(); + let _ = window; dialog .title("Update Minimum PIN Length") @@ -367,19 +411,20 @@ impl PasskeysView { .child( v_flex() .gap_2() - .child(format!("New PIN (min {} chars)", current_min)) + .child(format!("New PIN (min {} chars)", current_min)) .child(Input::new(&new)) ) .child("Confirm New PIN") .child(Input::new(&confirm)), ) + // on_ok is triggered by the Enter key (dialog binds Enter → Confirm action → on_ok). + // Return false so the dialog stays open; our submit closes it and opens a status dialog. + .on_ok(move |_, window, cx| { + submit_for_ok(window, cx); + false + }) .footer(move |_, _window, _cx, _| { - let view = view.clone(); - let current = current.clone(); - let new = new.clone(); - let confirm = confirm.clone(); - let slider = slider_handle.clone(); - + let s = submit_for_btn.clone(); vec![ Button::new("cancel") .label("Cancel") @@ -387,33 +432,8 @@ impl PasskeysView { Button::new("update") .primary() .label("Update") - .on_click(move |_, _, cx| { - let current_val = current.read(cx).text().to_string(); - let new_val = new.read(cx).text().to_string(); - let confirm_val = confirm.read(cx).text().to_string(); - let min_len = slider.read(cx).value().start() as u8; - - if current_val.is_empty() { - return; - } - - if !new_val.is_empty() { - if new_val != confirm_val { - let _ = view.update(cx, |_, cx| { - cx.emit(PasskeysEvent::Notification("PINs do not match".to_string())); - }); - return; - } - if new_val.len() < min_len as usize { - let _ = view.update(cx, |_, cx| { - cx.emit(PasskeysEvent::Notification(format!("PIN must be at least {} characters", min_len))); - }); - return; - } - } - let _ = view.update(cx, |this, cx| { - this.update_min_length(current_val, min_len, new_val, cx); - }); + .on_click(move |_, window, cx| { + s(window, cx); }), ] }) @@ -471,6 +491,7 @@ impl PasskeysView { current: String, min_len: u8, new_pin: String, + status_handle: WeakEntity, cx: &mut Context, ) { if self.loading { @@ -493,10 +514,9 @@ impl PasskeysView { log::error!("Failed to set minimum PIN length: {}", e); let _ = entity.update(cx, |this, cx| { this.loading = false; - cx.emit(PasskeysEvent::Notification(format!( - "Failed to set length: {}", - e - ))); + let _ = status_handle.update(cx, |s, cx| { + s.set_error(format!("Failed to set length: {}", e), cx); + }); cx.notify(); }); return; @@ -512,20 +532,21 @@ impl PasskeysView { match res_pin { Ok(_) => { log::info!("Minimum length and PIN updated successfully."); - cx.emit(PasskeysEvent::CloseDialog); - cx.emit(PasskeysEvent::Notification( - "Minimum length and PIN updated".to_string(), - )); if let Ok(info) = io::get_fido_info() { this.fido_info = Some(info); } + let _ = status_handle.update(cx, |s, cx| { + s.set_success("Minimum length and PIN updated.".to_string(), cx); + }); } Err(e) => { log::error!("Length set, but PIN change failed: {}", e); - cx.emit(PasskeysEvent::Notification(format!( - "Length set, but PIN change failed: {}", - e - ))); + let _ = status_handle.update(cx, |s, cx| { + s.set_error( + format!("Length set, but PIN change failed: {}", e), + cx, + ); + }); } } cx.notify(); @@ -534,14 +555,12 @@ impl PasskeysView { let _ = entity.update(cx, |this, cx| { this.loading = false; log::info!("Minimum PIN length updated to {}.", min_len); - cx.emit(PasskeysEvent::CloseDialog); - cx.emit(PasskeysEvent::Notification(format!( - "Minimum length updated to {}", - min_len - ))); if let Ok(info) = io::get_fido_info() { this.fido_info = Some(info); } + let _ = status_handle.update(cx, |s, cx| { + s.set_success(format!("Minimum length updated to {}.", min_len), cx); + }); cx.notify(); }); } @@ -930,16 +949,22 @@ impl PasskeysView { ), ) .child( - Button::new("delete-cred-btn") - .ghost() - .small() + div() + .on_mouse_down(MouseButton::Left, |_, _, cx| { + cx.stop_propagation(); + }) .child( - Icon::default() - .path("icons/trash-2.svg") - .size_4() - .text_color(theme.muted_foreground), - ) - .on_click(delete_listener), + Button::new("delete-cred-btn") + .ghost() + .small() + .child( + Icon::default() + .path("icons/trash-2.svg") + .size_4() + .text_color(theme.muted_foreground), + ) + .on_click(delete_listener), + ), ), ) }