diff --git a/Cargo.lock b/Cargo.lock index 5b9a35f2..169a722b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,43 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", + "zeroize", +] + +[[package]] +name = "aes-gcm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ctutils", + "ghash", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.8" @@ -178,6 +215,18 @@ dependencies = [ "rustversion", ] +[[package]] +name = "argon2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" +dependencies = [ + "base64ct", + "blake2 0.11.0", + "cpufeatures 0.3.0", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -532,6 +581,36 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.21.7" @@ -544,6 +623,23 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2 0.11.0", +] + [[package]] name = "beef" version = "0.5.2" @@ -601,13 +697,22 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" +dependencies = [ + "digest 0.11.2", +] + [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -615,6 +720,16 @@ name = "block-buffer" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" dependencies = [ "hybrid-array", ] @@ -641,6 +756,16 @@ dependencies = [ "piper", ] +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.6.1" @@ -837,6 +962,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.61" @@ -844,6 +978,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -899,8 +1035,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", + "cipher", "cpufeatures 0.3.0", "rand_core 0.10.1", + "zeroize", ] [[package]] @@ -944,6 +1082,18 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.6.1" @@ -980,6 +1130,21 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" @@ -1107,6 +1272,12 @@ dependencies = [ "libc", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1231,23 +1402,52 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.2", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.2", "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "crypto-primes" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" +dependencies = [ + "crypto-bigint", + "rand_core 0.10.1", ] [[package]] @@ -1300,6 +1500,53 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.2", + "fiat-crypto", + "rand_core 0.10.1", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.23.0" @@ -1348,6 +1595,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "deadpool" version = "0.12.3" @@ -1366,6 +1619,28 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1421,6 +1696,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.10.7" @@ -1440,7 +1724,8 @@ checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer 0.12.0", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -1599,12 +1884,75 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest 0.11.2", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.10.1", + "serde", + "sha2 0.11.0", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.2", + "ff", + "group", + "hkdf", + "hybrid-array", + "pem-rfc7468", + "pkcs8", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.8" @@ -1640,6 +1988,18 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1775,6 +2135,22 @@ dependencies = [ "log", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "field-offset" version = "0.3.6" @@ -1896,6 +2272,12 @@ dependencies = [ "futures-core", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2140,6 +2522,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "generic-array" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb130435a959a8d525e6bca66ff6c40981a300ee96d70e3ef56f046556d614a3" +dependencies = [ + "generic-array 0.14.7", + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2192,6 +2585,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", + "zeroize", +] + [[package]] name = "gio" version = "0.18.4" @@ -3219,6 +3622,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "gtk" version = "0.18.2" @@ -3404,6 +3818,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -3485,11 +3923,14 @@ checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ + "ctutils", + "subtle", "typenum", + "zeroize", ] [[package]] @@ -3789,6 +4230,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + [[package]] name = "interim" version = "0.2.1" @@ -3799,6 +4250,18 @@ dependencies = [ "logos", ] +[[package]] +name = "internal-russh-num-bigint" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.10.1", + "rand_core 0.10.1", +] + [[package]] name = "io-close" version = "0.3.7" @@ -3945,7 +4408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa8eadc56d459f48a58adcbdb66309b9d981f16816cc388cf91f27fd4fc4602a" dependencies = [ "async-trait", - "blake2", + "blake2 0.10.6", "bstr", "chrono", "clru", @@ -4041,6 +4504,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -4075,6 +4548,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -4441,6 +4934,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "md5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" + [[package]] name = "memchr" version = "2.8.0" @@ -4493,6 +4992,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8", + "rand_core 0.10.1", + "sha3 0.11.0", +] + [[package]] name = "mockall" version = "0.14.0" @@ -4519,6 +5032,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "muda" version = "0.17.2" @@ -4648,6 +5172,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -4997,7 +5533,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "portable-atomic", - "rand 0.9.4", + "rand 0.9.2", "thiserror 2.0.18", ] @@ -5027,6 +5563,47 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2 0.11.0", +] + +[[package]] +name = "p384" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2 0.11.0", +] + +[[package]] +name = "p521" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2 0.11.0", +] + [[package]] name = "page_size" version = "0.6.0" @@ -5037,6 +5614,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "pageant" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3adadc44070da6f464b0918655a12f5792c156e088d8c4082d13e27d94c3e791" +dependencies = [ + "base16ct", + "byteorder", + "bytes", + "delegate", + "futures", + "log", + "rand 0.10.1", + "sha2 0.11.0", + "thiserror 2.0.18", + "tokio", + "windows 0.62.2", + "windows-strings 0.5.1", +] + [[package]] name = "pango" version = "0.18.3" @@ -5091,12 +5688,40 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "phc", +] + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.2", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -5146,6 +5771,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", +] + [[package]] name = "phf" version = "0.8.0" @@ -5340,14 +5975,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "piper" -version = "0.2.5" +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "der", + "pbkdf2", + "rand_core 0.10.1", + "scrypt", + "sha2 0.11.0", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", + "der", + "pkcs5", + "rand_core 0.10.1", + "spki", ] [[package]] @@ -5436,6 +6110,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures 0.3.0", + "universal-hash", + "zeroize", +] + +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash", + "zeroize", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -5464,7 +6161,7 @@ dependencies = [ "lazy_static", "libc", "log", - "nix", + "nix 0.28.0", "serial2", "shared_library", "shell-words", @@ -5538,6 +6235,33 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -5779,9 +6503,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6083,6 +6807,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + [[package]] name = "rfd" version = "0.16.0" @@ -6117,7 +6851,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -6150,6 +6884,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rsa" +version = "0.10.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" +dependencies = [ + "const-oid", + "crypto-bigint", + "crypto-primes", + "digest 0.11.2", + "pkcs1", + "pkcs8", + "rand_core 0.10.1", + "sha2 0.11.0", + "signature", + "spki", + "zeroize", +] + [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -6175,6 +6928,102 @@ dependencies = [ "sqlite-wasm-rs", ] +[[package]] +name = "russh" +version = "0.63.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bab1b87d915817d5d9cc352637cd40d5f0b298a48c6309af9156a4addc3031" +dependencies = [ + "aes", + "aws-lc-rs", + "bitflags 2.11.1", + "block-padding", + "byteorder", + "bytes", + "cbc", + "cipher", + "crypto-bigint", + "ctr", + "curve25519-dalek", + "data-encoding", + "delegate", + "der", + "digest 0.11.2", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "flate2", + "futures", + "generic-array 1.4.2", + "getrandom 0.4.2", + "ghash", + "hex-literal", + "hmac", + "inout", + "internal-russh-num-bigint", + "keccak", + "log", + "md5", + "ml-kem", + "module-lattice", + "num-bigint", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2", + "pkcs1", + "pkcs5", + "pkcs8", + "polyval", + "rand 0.10.1", + "rand_core 0.10.1", + "rsa", + "russh-cryptovec", + "russh-util", + "salsa20", + "scrypt", + "sec1", + "sha1 0.11.0", + "sha2 0.11.0", + "sha3 0.12.0", + "signature", + "spki", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror 2.0.18", + "tokio", + "typenum", + "universal-hash", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" +dependencies = [ + "log", + "nix 0.31.3", + "ssh-encoding", + "windows-sys 0.61.2", +] + +[[package]] +name = "russh-util" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" +dependencies = [ + "chrono", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -6276,7 +7125,7 @@ checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -6291,6 +7140,16 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" +dependencies = [ + "cfg-if", + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -6357,12 +7216,38 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" +dependencies = [ + "cfg-if", + "pbkdf2", + "salsa20", + "sha2 0.11.0", +] + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + [[package]] name = "selectors" version = "0.24.0" @@ -6561,6 +7446,16 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "serial2" version = "0.2.36" @@ -6624,6 +7519,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + [[package]] name = "sha1-checked" version = "0.10.0" @@ -6631,7 +7537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ "digest 0.10.7", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -6656,6 +7562,27 @@ dependencies = [ "digest 0.11.2", ] +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.2", + "keccak", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.2", + "keccak", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -6707,6 +7634,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.2", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -6804,6 +7741,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlite-wasm-rs" version = "0.5.3" @@ -6816,6 +7769,65 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ssh-cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "chacha20", + "cipher", + "ctutils", + "des", + "poly1305", + "ssh-encoding", + "zeroize", +] + +[[package]] +name = "ssh-encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" +dependencies = [ + "base64ct", + "bytes", + "crypto-bigint", + "ctutils", + "digest 0.11.2", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "ssh-key" +version = "0.7.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" +dependencies = [ + "argon2", + "bcrypt-pbkdf", + "ctutils", + "ed25519-dalek", + "hex", + "hmac", + "p256", + "p384", + "p521", + "rand_core 0.10.1", + "rsa", + "sec1", + "sha1 0.11.0", + "sha2 0.11.0", + "signature", + "ssh-cipher", + "ssh-encoding", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7025,7 +8037,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -7114,7 +8126,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -7315,7 +8327,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", + "windows 0.61.3", "zbus", ] @@ -7341,7 +8353,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -7366,7 +8378,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -7910,10 +8922,12 @@ name = "treq" version = "0.3.0" dependencies = [ "async-trait", + "bytes", "chrono", "criterion", "duckdb", "futures", + "getrandom 0.4.2", "gix", "ignore", "imara-diff 0.1.8", @@ -7930,10 +8944,12 @@ dependencies = [ "opentelemetry-stdout", "opentelemetry_sdk", "portable-pty", + "rand_core 0.10.1", "rayon", "regex", "reqwest 0.12.28", "rusqlite", + "russh", "serde", "serde_json", "serde_yaml", @@ -8081,12 +9097,28 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -8488,7 +9520,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -8512,7 +9544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -8568,11 +9600,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -8584,6 +9628,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -8618,7 +9671,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -8665,6 +9729,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -8814,6 +9888,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -9134,6 +10217,17 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.3" @@ -9178,7 +10272,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/Cargo.toml b/Cargo.toml index 55601a6c..6b713a7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,4 @@ members = ["src-tauri"] resolver = "3" [patch.crates-io] -tauri-test-macros = { path = "crates/tauri-test-macros" } \ No newline at end of file +tauri-test-macros = { path = "crates/tauri-test-macros" } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5ba2d764..96ad2d4d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -63,12 +63,16 @@ duckdb = "1.10505.0" tempfile = { version = "3", optional = true } async-trait = "0.1.92" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time"] } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time", "io-util", "net", "sync"] } +russh = "0.63.1" +bytes = "1" [dev-dependencies] mockall = "0.14.0" tempfile = "3" criterion = { version = "0.8.2", features = ["html_reports"] } +getrandom = "0.4" +rand_core = "0.10" wiremock = "0.6" [[bench]] diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index b12dc5c5..7021d189 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -11,6 +11,7 @@ pub mod remote_bootstrap; pub mod remote_control_plane; pub mod remote_provider; pub mod remote_provider_sprites; +pub mod remote_ssh_transport; pub mod repo; pub mod resolve; pub mod stash; diff --git a/src-tauri/src/core/remote_ssh_transport.rs b/src-tauri/src/core/remote_ssh_transport.rs new file mode 100644 index 00000000..60f2a5c9 --- /dev/null +++ b/src-tauri/src/core/remote_ssh_transport.rs @@ -0,0 +1,1219 @@ +//! Native Rust SSH transport (Phase 4: "Native desktop SSH"). +//! +//! Replaces system `ssh` subprocesses with the pure-Rust [`russh`] client for +//! the managed transport path. This module owns exactly the responsibilities +//! listed under the PRD's "Native SSH transport" section: +//! +//! - strict host-key verification against Phase 3's [`TrustedHostKey`]s; +//! - user-key and certificate authentication ([`SshAuthentication`]); +//! - connection pooling and channel multiplexing; +//! - keepalives, reconnect, and stale-connection recovery; +//! - exec channels (non-interactive, JSON-producing) with deadlines, +//! cancellation, and output limits; +//! - PTY channels for interactive shells and agents, with resize support. +//! +//! It consumes the domain types from [`crate::core::remote_control_plane`] +//! (`SshEndpoint`, `TrustedHostKey`, `SshEndpointSource`, `SshAuthentication`) +//! rather than redefining them. It does not implement JJ, Git, workspace, or +//! any other repository behavior: callers hand it a fully-formed +//! `treq --format=json` argument vector and get back raw +//! stdout/stderr bytes plus an exit status; interpreting that JSON is Phase +//! 5's job. +//! +//! SFTP and port forwarding are out of scope, per the PRD. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use russh::client::{AuthResult, Handle, Handler as ClientHandlerTrait}; +use russh::keys::{PrivateKey, PrivateKeyWithHashAlg}; +use russh::{ChannelMsg, Disconnect}; +use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex as AsyncMutex; +use tokio::sync::Notify; + +use crate::core::remote_control_plane::{SshAuthentication, SshEndpoint, TrustedHostKey}; + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +/// Errors surfaced by the native transport. Kept distinct from +/// [`crate::core::remote::TransportError`] (the older, subprocess-based +/// transport's error type) so a Phase 5 caller can pattern-match without +/// pulling in `std::process` semantics that no longer apply. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SshTransportError { + /// The presented host key did not match any trusted fingerprint recorded + /// for this endpoint. Never bypassed, never auto-trusted. + HostKeyMismatch { + endpoint_id: String, + presented_fingerprint: String, + }, + /// TCP connect, key exchange, or protocol negotiation failed. + ConnectionFailed(String), + /// Authentication (key or certificate) was rejected by the server. + AuthenticationFailed(String), + /// Key or certificate material could not be loaded from local disk. + KeyMaterialUnavailable(String), + /// The exec channel exceeded its total operation deadline. + DeadlineExceeded, + /// The exec channel produced more stdout/stderr bytes than the configured + /// limit before completing. + OutputLimitExceeded, + /// The caller cancelled the operation (e.g. dropped a `CancellationToken`). + Cancelled, + /// The remote command exited non-zero. Carries stderr so a Phase 5 caller + /// can surface diagnostics without re-parsing stdout. + CommandFailed { + exit_status: Option, + stderr: String, + }, + /// stdout was not valid UTF-8 / valid JSON once decoded by the caller. + ProtocolError(String), + /// Underlying channel or session I/O failure, e.g. after a stale + /// connection is detected mid-operation. + ChannelError(String), +} + +impl std::fmt::Display for SshTransportError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::HostKeyMismatch { endpoint_id, .. } => { + write!(f, "host key mismatch for endpoint {endpoint_id}") + } + Self::ConnectionFailed(message) => write!(f, "ssh connection failed: {message}"), + Self::AuthenticationFailed(message) => write!(f, "ssh authentication failed: {message}"), + Self::KeyMaterialUnavailable(message) => write!(f, "ssh key material unavailable: {message}"), + Self::DeadlineExceeded => write!(f, "ssh operation exceeded its deadline"), + Self::OutputLimitExceeded => write!(f, "ssh command output exceeded the configured limit"), + Self::Cancelled => write!(f, "ssh operation was cancelled"), + Self::CommandFailed { + exit_status, + stderr, + } => { + write!(f, "remote command failed (exit={exit_status:?}): {stderr}") + } + Self::ProtocolError(message) => write!(f, "invalid remote command protocol: {message}"), + Self::ChannelError(message) => write!(f, "ssh channel error: {message}"), + } + } +} + +impl std::error::Error for SshTransportError {} + +/// [`ClientHandlerTrait::Error`] must implement `From`. +impl From for SshTransportError { + fn from(error: russh::Error) -> Self { + Self::ChannelError(error.to_string()) + } +} + +// --------------------------------------------------------------------------- +// Host-key verification +// --------------------------------------------------------------------------- + +/// Computes the SHA256 fingerprint string (`SHA256:`) for a server +/// host key or certificate, in the same shape as [`TrustedHostKey`]'s +/// `fingerprint_sha256`. +fn fingerprint_of(key: &russh::keys::PublicKeyOrCertificate) -> Option { + match key { + russh::keys::PublicKeyOrCertificate::PublicKey { key, .. } => { + Some(key.fingerprint(russh::keys::HashAlg::Sha256).to_string()) + } + // Server host certificates are not part of the Phase 3 trust model + // (`TrustedHostKey` pins bare host keys); rejecting them is the strict, + // never-bypass default rather than silently trusting an unpinned CA. + russh::keys::PublicKeyOrCertificate::Certificate(_) => None, + } +} + +/// Strict host-key verification against a fixed set of trusted fingerprints +/// for one endpoint/generation. Never falls back to trust-on-first-use and +/// never consults the OS `~/.ssh/known_hosts` — managed host trust is tracked +/// independently, per the PRD's "Host-key verification" section. +#[derive(Debug, Clone)] +pub struct HostKeyVerifier { + endpoint_id: String, + trusted_fingerprints: Vec, +} + +impl HostKeyVerifier { + pub fn new(endpoint_id: impl Into, host_keys: &[TrustedHostKey]) -> Self { + Self { + endpoint_id: endpoint_id.into(), + trusted_fingerprints: host_keys + .iter() + .map(|key| key.fingerprint_sha256.clone()) + .collect(), + } + } + + /// Returns `Ok(())` when `presented` matches one of the trusted + /// fingerprints, `Err` (never a bypassable warning) otherwise. + pub fn verify( + &self, + presented: &russh::keys::PublicKeyOrCertificate, + ) -> Result<(), SshTransportError> { + let Some(fingerprint) = fingerprint_of(presented) else { + return Err(SshTransportError::HostKeyMismatch { + endpoint_id: self.endpoint_id.clone(), + presented_fingerprint: "".to_string(), + }); + }; + if self + .trusted_fingerprints + .iter() + .any(|trusted| trusted == &fingerprint) + { + Ok(()) + } else { + Err(SshTransportError::HostKeyMismatch { + endpoint_id: self.endpoint_id.clone(), + presented_fingerprint: fingerprint, + }) + } + } +} + +// --------------------------------------------------------------------------- +// Client handler +// --------------------------------------------------------------------------- + +/// `russh::client::Handler` implementation. Holds only what is needed to +/// verify the server's host key; it performs no logging of key material and +/// carries no secrets itself (those live in [`ClientAuthenticator`]). +#[derive(Clone)] +struct TreqSshClientHandler { + verifier: HostKeyVerifier, +} + +impl ClientHandlerTrait for TreqSshClientHandler { + type Error = SshTransportError; + + async fn check_server_key( + &mut self, + server_public_key: &russh::keys::PublicKeyOrCertificate, + ) -> Result { + match self.verifier.verify(server_public_key) { + Ok(()) => Ok(true), + Err(error) => { + // Redaction: log only the endpoint id and fingerprint (public, + // non-sensitive values), never key material. + tracing::warn!( + endpoint_id = %self.verifier.endpoint_id, + error = %error, + "ssh host key verification failed" + ); + // Returning Ok(false) lets russh reject the connection cleanly + // instead of propagating an error that could look like a transport + // fault; the caller still learns about the mismatch because + // `connect_verified` re-checks and returns `Err` explicitly below. + Ok(false) + } + } + } +} + +// --------------------------------------------------------------------------- +// Authentication +// --------------------------------------------------------------------------- + +/// Loads client key material for an [`SshEndpoint`]'s [`SshAuthentication`]. +/// +/// Key material never lives in Supabase or any control-plane record (PRD: +/// "Treq never generates a user private key" / "Supabase stores public keys +/// ... only"). The `key_reference` string names a private key already on the +/// user's device. Two forms are accepted, matching how `remote.rs` already +/// treats host references as opaque, pre-existing local configuration rather +/// than something Treq synthesizes: +/// +/// - an absolute or `~`-relative path to an OpenSSH private key file; +/// - a bare filename, resolved under `~/.ssh/`. +/// +/// For certificate authentication, the signed certificate is expected next +/// to the private key as `-cert.pub` (the standard OpenSSH +/// convention `ssh-keygen -s` produces), unless `key_reference` already +/// points at a `-cert.pub` file in which case the private key is the same +/// path with that suffix stripped. +pub struct ClientAuthenticator; + +impl ClientAuthenticator { + fn resolve_private_key_path(key_reference: &str) -> PathBuf { + let reference = key_reference + .strip_suffix("-cert.pub") + .unwrap_or(key_reference); + let expanded = if let Some(rest) = reference.strip_prefix("~/") { + dirs_home().join(rest) + } else { + PathBuf::from(reference) + }; + if expanded.is_absolute() || expanded.components().count() > 1 { + expanded + } else { + dirs_home().join(".ssh").join(expanded) + } + } + + fn load_private_key(key_reference: &str) -> Result { + let path = Self::resolve_private_key_path(key_reference); + russh::keys::load_secret_key(&path, None).map_err(|error| { + // Never log the path's contents; the path itself is not secret. + SshTransportError::KeyMaterialUnavailable(format!( + "failed to load private key from {}: {error}", + path.display() + )) + }) + } + + fn load_certificate(key_reference: &str) -> Result { + let key_path = Self::resolve_private_key_path(key_reference); + let cert_path = PathBuf::from(format!("{}-cert.pub", key_path.display())); + russh::keys::load_openssh_certificate(&cert_path).map_err(|error| { + SshTransportError::KeyMaterialUnavailable(format!( + "failed to load certificate from {}: {error}", + cert_path.display() + )) + }) + } + + /// Authenticates `handle` per `authentication`, using `username` from the + /// endpoint. Returns an error rather than silently downgrading to a + /// weaker method on failure. + async fn authenticate( + handle: &mut Handle, + username: &str, + authentication: &SshAuthentication, + ) -> Result<(), SshTransportError> { + let result = match authentication { + SshAuthentication::PublicKey { key_reference } => { + let key = Arc::new(Self::load_private_key(key_reference)?); + let hash_alg = handle + .best_supported_rsa_hash() + .await + .ok() + .flatten() + .flatten(); + handle + .authenticate_publickey(username, PrivateKeyWithHashAlg::new(key, hash_alg)) + .await + .map_err(|error| SshTransportError::AuthenticationFailed(error.to_string()))? + } + SshAuthentication::Certificate { key_reference } => { + let key = Arc::new(Self::load_private_key(key_reference)?); + let cert = Self::load_certificate(key_reference)?; + handle + .authenticate_openssh_cert(username, key, cert) + .await + .map_err(|error| SshTransportError::AuthenticationFailed(error.to_string()))? + } + }; + match result { + AuthResult::Success => Ok(()), + AuthResult::Failure { .. } => Err(SshTransportError::AuthenticationFailed( + "server rejected the presented key or certificate".to_string(), + )), + } + } +} + +fn dirs_home() -> PathBuf { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) +} + +// --------------------------------------------------------------------------- +// Connection pooling +// --------------------------------------------------------------------------- + +/// Pool key derivation. Per the PRD: "Repository identity references the +/// endpoint ID ... generation", so the pool is keyed on more than a hostname +/// — endpoint id, generation, and the connection parameters that would +/// actually require a new TCP/SSH session if they changed. Two endpoints +/// that happen to share a hostname (e.g. after a reprovision that reused an +/// address) must never share a pooled connection across a generation +/// boundary, and a host-key rotation must never silently keep an old, +/// now-untrusted connection alive. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PoolKey { + endpoint_id: String, + generation: u64, + hostname: String, + port: u16, + username: String, + host_key_fingerprints: Vec, +} + +impl PoolKey { + pub fn for_endpoint(endpoint: &SshEndpoint) -> Self { + let generation = match &endpoint.source { + crate::core::remote_control_plane::SshEndpointSource::Managed { generation, .. } => { + *generation + } + _ => 0, + }; + let mut host_key_fingerprints: Vec = endpoint + .host_keys + .iter() + .map(|key| key.fingerprint_sha256.clone()) + .collect(); + host_key_fingerprints.sort(); + Self { + endpoint_id: endpoint.id.clone(), + generation, + hostname: endpoint.hostname.clone(), + port: endpoint.port, + username: endpoint.username.clone(), + host_key_fingerprints, + } + } +} + +struct PooledConnection { + handle: Arc>>, + last_used: Instant, + alive: Arc, +} + +/// Pooled, multiplexed SSH connection manager. One authenticated connection +/// per [`PoolKey`] is reused for multiple exec and PTY channels rather than +/// reconnecting per command (PRD: "the native SSH transport reuses a +/// connection for multiple structured commands"). +pub struct SshConnectionPool { + connections: AsyncMutex>, + keepalive_interval: Duration, + idle_timeout: Duration, +} + +impl Default for SshConnectionPool { + fn default() -> Self { + Self::new() + } +} + +impl SshConnectionPool { + pub fn new() -> Self { + Self { + connections: AsyncMutex::new(HashMap::new()), + keepalive_interval: Duration::from_secs(30), + idle_timeout: Duration::from_secs(600), + } + } + + /// Returns a live authenticated connection for `endpoint`, reusing a + /// pooled one when present and not marked dead. A dead or missing + /// connection is transparently reconnected here — this is safe because + /// establishing a connection is not a mutation with side effects on the + /// remote system, unlike retrying an in-flight command (see + /// `exec_command`'s cancellation handling, which never auto-retries). + async fn get_or_connect( + &self, + endpoint: &SshEndpoint, + ) -> Result>>, SshTransportError> { + let key = PoolKey::for_endpoint(endpoint); + { + let mut connections = self.connections.lock().await; + if let Some(existing) = connections.get_mut(&key) { + if existing.alive.load(Ordering::SeqCst) { + existing.last_used = Instant::now(); + return Ok(existing.handle.clone()); + } + tracing::info!(endpoint_id = %endpoint.id, "pooled ssh connection is dead, reconnecting"); + connections.remove(&key); + } + } + + let handle = self.connect(endpoint).await?; + let alive = Arc::new(AtomicBool::new(true)); + let mut connections = self.connections.lock().await; + connections.insert( + key, + PooledConnection { + handle: handle.clone(), + last_used: Instant::now(), + alive, + }, + ); + Ok(handle) + } + + async fn connect( + &self, + endpoint: &SshEndpoint, + ) -> Result>>, SshTransportError> { + let verifier = HostKeyVerifier::new(endpoint.id.clone(), &endpoint.host_keys); + let handler = TreqSshClientHandler { verifier }; + + let mut config = russh::client::Config::default(); + config.keepalive_interval = Some(self.keepalive_interval); + config.keepalive_max = 3; + let config = Arc::new(config); + + let address = (endpoint.hostname.as_str(), endpoint.port); + tracing::debug!(endpoint_id = %endpoint.id, hostname = %endpoint.hostname, port = endpoint.port, "opening ssh connection"); + + let mut handle = russh::client::connect(config, address, handler) + .await + .map_err(|error| SshTransportError::ConnectionFailed(error.to_string()))?; + + ClientAuthenticator::authenticate(&mut handle, &endpoint.username, &endpoint.authentication) + .await?; + + tracing::info!(endpoint_id = %endpoint.id, "ssh connection established and authenticated"); + Ok(Arc::new(AsyncMutex::new(handle))) + } + + /// Marks the pooled connection for `endpoint` dead, e.g. after an I/O + /// error observed mid-operation. The next `get_or_connect` call for the + /// same key reconnects rather than reusing a stale session. + pub async fn mark_dead(&self, endpoint: &SshEndpoint) { + let key = PoolKey::for_endpoint(endpoint); + let connections = self.connections.lock().await; + if let Some(pooled) = connections.get(&key) { + pooled.alive.store(false, Ordering::SeqCst); + } + } + + /// Drops idle pooled connections that have exceeded the idle timeout, + /// closing them cleanly. Intended to be driven by a periodic background + /// task; exposed here as a plain method so callers control scheduling. + pub async fn sweep_idle(&self) { + let now = Instant::now(); + let mut connections = self.connections.lock().await; + let stale: Vec = connections + .iter() + .filter(|(_, pooled)| now.duration_since(pooled.last_used) > self.idle_timeout) + .map(|(key, _)| key.clone()) + .collect(); + for key in stale { + if let Some(pooled) = connections.remove(&key) { + let handle = pooled.handle.lock().await; + let _ = handle + .disconnect(Disconnect::ByApplication, "idle timeout", "en") + .await; + } + } + } + + pub async fn pooled_connection_count(&self) -> usize { + self.connections.lock().await.len() + } +} + +// --------------------------------------------------------------------------- +// Exec channels +// --------------------------------------------------------------------------- + +/// Result of a completed exec channel invocation. stdout and stderr are kept +/// separate per the PRD's structured command protocol ("stdout contains only +/// the result JSON; stderr contains diagnostics"). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecOutput { + pub stdout: Vec, + pub stderr: Vec, + pub exit_status: Option, +} + +impl ExecOutput { + pub fn success(&self) -> bool { + matches!(self.exit_status, Some(0)) + } +} + +/// A cooperative cancellation flag for in-flight exec/PTY operations. A +/// caller drops or flips this to cancel; the transport never itself decides +/// to retry a mutation that isn't provably idempotent (PRD: "mutations +/// accept idempotency keys where retry could duplicate work") — cancellation +/// always surfaces as [`SshTransportError::Cancelled`], never a silent retry. +#[derive(Clone, Default)] +pub struct CancellationToken { + cancelled: Arc, + notify: Arc, +} + +impl CancellationToken { + pub fn new() -> Self { + Self::default() + } + + pub fn cancel(&self) { + self.cancelled.store(true, Ordering::SeqCst); + self.notify.notify_waiters(); + } + + pub fn is_cancelled(&self) -> bool { + self.cancelled.load(Ordering::SeqCst) + } + + async fn cancelled(&self) { + if self.is_cancelled() { + return; + } + self.notify.notified().await; + } +} + +/// Bounds applied to every exec channel invocation. +#[derive(Debug, Clone, Copy)] +pub struct ExecLimits { + pub deadline: Duration, + /// Maximum combined stdout+stderr bytes before the channel is aborted. + pub max_output_bytes: usize, +} + +impl Default for ExecLimits { + fn default() -> Self { + Self { + deadline: Duration::from_secs(30), + max_output_bytes: 8 * 1024 * 1024, + } + } +} + +/// Runs `treq --format=json` as a non-interactive exec channel on +/// the pooled connection for `endpoint`. No frontend-provided arbitrary +/// command ever reaches this method directly — callers must build `args` +/// through a typed request (see `crate::core::remote::TreqCommandRequest`), +/// never by interpolating raw UI text. +pub async fn exec_command( + pool: &SshConnectionPool, + endpoint: &SshEndpoint, + args: &[String], + limits: ExecLimits, + cancellation: &CancellationToken, +) -> Result { + if cancellation.is_cancelled() { + return Err(SshTransportError::Cancelled); + } + + let handle = pool.get_or_connect(endpoint).await?; + + let run = async { + let session = handle.lock().await; + let mut channel = session + .channel_open_session() + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?; + drop(session); + + let command = build_remote_command_line(args); + // The command line is assembled from separate typed arguments (see + // `TreqCommandRequest::cli_args`) and passed as a single exec string per + // the SSH exec protocol; no frontend text is interpolated as shell here + // beyond the same argument-vector quoting the CLI already validates. + channel + .exec(true, command) + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?; + + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut exit_status = None; + + loop { + let Some(message) = channel.wait().await else { + break; + }; + match message { + ChannelMsg::Data { data } => { + stdout.extend_from_slice(&data); + } + ChannelMsg::ExtendedData { data, .. } => { + stderr.extend_from_slice(&data); + } + ChannelMsg::ExitStatus { + exit_status: status, + } => { + exit_status = Some(status); + } + ChannelMsg::Eof | ChannelMsg::Close => { + break; + } + _ => {} + } + if stdout.len() + stderr.len() > limits.max_output_bytes { + return Err(SshTransportError::OutputLimitExceeded); + } + } + + Ok(ExecOutput { + stdout, + stderr, + exit_status, + }) + }; + + let outcome = tokio::select! { + biased; + _ = cancellation.cancelled() => Err(SshTransportError::Cancelled), + result = tokio::time::timeout(limits.deadline, run) => match result { + Ok(inner) => inner, + Err(_) => Err(SshTransportError::DeadlineExceeded), + }, + }; + + match &outcome { + Err(SshTransportError::ChannelError(_)) => pool.mark_dead(endpoint).await, + _ => {} + } + + let output = outcome?; + if !output.success() { + return Err(SshTransportError::CommandFailed { + exit_status: output.exit_status, + stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), + }); + } + Ok(output) +} + +/// Builds the exec command line from an argument vector using the same +/// single-quote escaping `core::remote::shell_quote` uses, so a remote shell +/// that wraps `treq` (or plain `exec`) still sees each argument intact. +fn build_remote_command_line(args: &[String]) -> String { + let mut parts = vec!["treq".to_string()]; + parts.extend(args.iter().map(|arg| crate::core::remote::shell_quote(arg))); + parts.join(" ") +} + +// --------------------------------------------------------------------------- +// PTY channels +// --------------------------------------------------------------------------- + +/// An open interactive PTY channel (shell or agent process) on the pooled +/// connection for one endpoint. Read/write/resize/close mirror the shape of +/// [`crate::pty::PtySession`] so a later (Phase 6) consumer that already +/// knows the local PTY API is not learning a second vocabulary, even though +/// the two are separate transports. +pub struct RemotePtyChannel { + channel: AsyncMutex>, +} + +impl RemotePtyChannel { + /// Opens a PTY channel and requests a shell (or, when `command` is set, an + /// exec'd interactive process such as an agent) inside it. + pub async fn open( + pool: &SshConnectionPool, + endpoint: &SshEndpoint, + term: &str, + cols: u16, + rows: u16, + command: Option<&str>, + ) -> Result { + let handle = pool.get_or_connect(endpoint).await?; + let session = handle.lock().await; + let channel = session + .channel_open_session() + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?; + channel + .request_pty(true, term, cols as u32, rows as u32, 0, 0, &[]) + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?; + match command { + Some(command) => channel + .exec(true, command.as_bytes()) + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?, + None => channel + .request_shell(true) + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?, + } + Ok(Self { + channel: AsyncMutex::new(channel), + }) + } + + /// Writes raw bytes to the remote PTY (keystrokes, pasted input, etc.). + /// Never logs `data`, per the PRD's "raw terminal output ... by default" + /// never-log requirement. + pub async fn write(&self, data: &[u8]) -> Result<(), SshTransportError> { + let channel = self.channel.lock().await; + let mut writer = channel.make_writer(); + writer + .write_all(data) + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string()))?; + Ok(()) + } + + /// Reads the next chunk of output, or `None` once the channel has closed. + /// Callers own their own output buffering and redaction policy for what + /// they do with the bytes (e.g. terminal echo vs. a log line). + pub async fn read_chunk(&self) -> Result>, SshTransportError> { + let mut channel = self.channel.lock().await; + loop { + match channel.wait().await { + Some(ChannelMsg::Data { data }) => return Ok(Some(data.to_vec())), + Some(ChannelMsg::ExtendedData { data, .. }) => return Ok(Some(data.to_vec())), + Some(ChannelMsg::Eof) | Some(ChannelMsg::Close) | None => return Ok(None), + Some(_) => continue, + } + } + } + + pub async fn resize(&self, cols: u16, rows: u16) -> Result<(), SshTransportError> { + let channel = self.channel.lock().await; + channel + .window_change(cols as u32, rows as u32, 0, 0) + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string())) + } + + pub async fn close(&self) -> Result<(), SshTransportError> { + let channel = self.channel.lock().await; + channel + .close() + .await + .map_err(|error| SshTransportError::ChannelError(error.to_string())) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::remote_control_plane::{SshEndpointSource, TrustedHostKey}; + use russh::keys::PrivateKey; + use russh::server::{self, Msg as ServerMsg, Server as _, Session as ServerSession}; + use russh::{Channel, ChannelId}; + use std::sync::atomic::AtomicUsize; + + fn test_host_key() -> PrivateKey { + use getrandom::SysRng; + use rand_core::UnwrapErr; + PrivateKey::random(&mut UnwrapErr(SysRng), russh::keys::Algorithm::Ed25519).unwrap() + } + + fn trusted_host_key(key: &PrivateKey) -> TrustedHostKey { + let fingerprint = key + .public_key() + .fingerprint(russh::keys::HashAlg::Sha256) + .to_string(); + TrustedHostKey { + algorithm: "ssh-ed25519".to_string(), + fingerprint_sha256: fingerprint, + comment: None, + } + } + + // -- Host-key verification ------------------------------------------------- + + #[test] + fn host_key_verifier_accepts_matching_fingerprint() { + let host_key = test_host_key(); + let trusted = trusted_host_key(&host_key); + let verifier = HostKeyVerifier::new("endpoint-1", std::slice::from_ref(&trusted)); + + let presented = russh::keys::PublicKeyOrCertificate::PublicKey { + key: host_key.public_key().clone(), + hash_alg: None, + }; + + assert!(verifier.verify(&presented).is_ok()); + } + + #[test] + fn host_key_verifier_rejects_mismatched_fingerprint() { + let trusted_key = test_host_key(); + let presented_key = test_host_key(); + let trusted = trusted_host_key(&trusted_key); + let verifier = HostKeyVerifier::new("endpoint-1", std::slice::from_ref(&trusted)); + + let presented = russh::keys::PublicKeyOrCertificate::PublicKey { + key: presented_key.public_key().clone(), + hash_alg: None, + }; + + let error = verifier.verify(&presented).unwrap_err(); + assert!(matches!(error, SshTransportError::HostKeyMismatch { .. })); + } + + #[test] + fn host_key_verifier_rejects_when_no_trusted_keys_are_recorded() { + let presented_key = test_host_key(); + let verifier = HostKeyVerifier::new("endpoint-1", &[]); + + let presented = russh::keys::PublicKeyOrCertificate::PublicKey { + key: presented_key.public_key().clone(), + hash_alg: None, + }; + + // No bypass flag exists: an endpoint with zero trusted keys rejects + // every presented key rather than accepting on first use. + assert!(verifier.verify(&presented).is_err()); + } + + // -- Pool key derivation ---------------------------------------------------- + + fn sample_endpoint(generation: u64, hostname: &str, fingerprint: &str) -> SshEndpoint { + SshEndpoint { + id: "endpoint-1".to_string(), + instance_id: Some("instance-1".to_string()), + source: SshEndpointSource::Managed { + provider: "fly_sprites".to_string(), + generation, + }, + hostname: hostname.to_string(), + port: 22, + username: "treq".to_string(), + host_keys: vec![TrustedHostKey { + algorithm: "ssh-ed25519".to_string(), + fingerprint_sha256: fingerprint.to_string(), + comment: None, + }], + authentication: SshAuthentication::PublicKey { + key_reference: "id_ed25519".to_string(), + }, + } + } + + #[test] + fn pool_key_differs_across_generations_for_same_hostname() { + let gen1 = sample_endpoint(1, "10.0.0.5", "SHA256:aaa"); + let gen2 = sample_endpoint(2, "10.0.0.5", "SHA256:bbb"); + assert_ne!(PoolKey::for_endpoint(&gen1), PoolKey::for_endpoint(&gen2)); + } + + #[test] + fn pool_key_is_stable_for_identical_endpoints() { + let a = sample_endpoint(1, "10.0.0.5", "SHA256:aaa"); + let b = sample_endpoint(1, "10.0.0.5", "SHA256:aaa"); + assert_eq!(PoolKey::for_endpoint(&a), PoolKey::for_endpoint(&b)); + } + + #[test] + fn pool_key_ignores_host_key_fingerprint_ordering() { + let mut a = sample_endpoint(1, "10.0.0.5", "SHA256:aaa"); + a.host_keys.push(TrustedHostKey { + algorithm: "ssh-rsa".to_string(), + fingerprint_sha256: "SHA256:zzz".to_string(), + comment: None, + }); + let mut b = a.clone(); + b.host_keys.reverse(); + assert_eq!(PoolKey::for_endpoint(&a), PoolKey::for_endpoint(&b)); + } + + // -- Cancellation ------------------------------------------------------------- + + #[tokio::test] + async fn cancellation_token_reports_cancelled_immediately_when_pre_cancelled() { + let token = CancellationToken::new(); + token.cancel(); + assert!(token.is_cancelled()); + // `cancelled()` must resolve immediately rather than hang, even though + // `cancel()` happened before anyone called `cancelled()`. + tokio::time::timeout(Duration::from_millis(50), token.cancelled()) + .await + .expect("cancelled() should resolve without waiting for a fresh notify"); + } + + // -- In-process mock SSH server for exec-channel behavior -------------------- + + #[derive(Clone)] + struct MockServer { + host_key_algo: &'static str, + reply: Arc, // 0 = normal echo, 1 = slow (for deadline test), 2 = huge output + } + + impl server::Server for MockServer { + type Handler = MockHandler; + fn new_client(&mut self, _: Option) -> MockHandler { + MockHandler { + mode: self.reply.clone(), + } + } + } + + struct MockHandler { + mode: Arc, + } + + impl server::Handler for MockHandler { + type Error = russh::Error; + + async fn auth_publickey( + &mut self, + _user: &str, + _key: &russh::keys::ssh_key::PublicKey, + ) -> Result { + Ok(server::Auth::Accept) + } + + async fn channel_open_session( + &mut self, + _channel: Channel, + reply: server::ChannelOpenHandle, + _session: &mut ServerSession, + ) -> Result<(), Self::Error> { + reply.accept().await; + Ok(()) + } + + async fn exec_request( + &mut self, + channel: ChannelId, + data: &[u8], + session: &mut ServerSession, + ) -> Result<(), Self::Error> { + let command = String::from_utf8_lossy(data).to_string(); + session.channel_success(channel)?; + match self.mode.load(Ordering::SeqCst) { + 1 => { + // Slow path: sleep past the client's deadline before ever + // replying, to exercise deadline enforcement. + tokio::time::sleep(Duration::from_secs(5)).await; + session.data(channel, bytes::Bytes::from_static(b"{}"))?; + session.exit_status_request(channel, 0)?; + } + 2 => { + // Oversized path: exceed the client's output limit. + let chunk = vec![b'x'; 4096]; + for _ in 0..64 { + session.data(channel, bytes::Bytes::from(chunk.clone()))?; + } + session.exit_status_request(channel, 0)?; + } + _ => { + let response = format!("{{\"echo\":\"{command}\"}}"); + session.data(channel, bytes::Bytes::from(response.into_bytes()))?; + session.exit_status_request(channel, 0)?; + } + } + session.close(channel)?; + Ok(()) + } + } + + async fn start_mock_server(mode: usize) -> (std::net::SocketAddr, PrivateKey) { + let host_key = test_host_key(); + let mut config = server::Config::default(); + config.keys.push(host_key.clone()); + config.auth_rejection_time = Duration::from_millis(10); + let config = Arc::new(config); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + let mut server = MockServer { + host_key_algo: "ssh-ed25519", + reply: Arc::new(AtomicUsize::new(mode)), + }; + let _ = server.host_key_algo; + + tokio::spawn(async move { + loop { + let Ok((socket, peer)) = listener.accept().await else { + break; + }; + let handler = server.new_client(Some(peer)); + let config = config.clone(); + tokio::spawn(async move { + let _ = server::run_stream(config, socket, handler).await; + }); + } + }); + + (addr, host_key) + } + + fn write_client_key(dir: &std::path::Path, key: &PrivateKey) -> String { + let path = dir.join("id_test"); + let pem = key + .to_openssh(russh::keys::ssh_key::LineEnding::LF) + .unwrap(); + std::fs::write(&path, pem.as_bytes()).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); + } + path.to_string_lossy().into_owned() + } + + fn test_endpoint( + addr: std::net::SocketAddr, + host_key: &PrivateKey, + key_reference: String, + ) -> SshEndpoint { + let fingerprint = host_key + .public_key() + .fingerprint(russh::keys::HashAlg::Sha256) + .to_string(); + SshEndpoint { + id: "test-endpoint".to_string(), + instance_id: None, + source: SshEndpointSource::UserManaged, + hostname: addr.ip().to_string(), + port: addr.port(), + username: std::env::var("USER").unwrap_or_else(|_| "user".to_string()), + host_keys: vec![TrustedHostKey { + algorithm: "ssh-ed25519".to_string(), + fingerprint_sha256: fingerprint, + comment: None, + }], + authentication: SshAuthentication::PublicKey { key_reference }, + } + } + + #[tokio::test] + async fn exec_command_returns_stdout_and_reuses_pooled_connection() { + let (addr, host_key) = start_mock_server(0).await; + let client_key = test_host_key(); + let temp_dir = tempfile::tempdir().unwrap(); + let key_reference = write_client_key(temp_dir.path(), &client_key); + let endpoint = test_endpoint(addr, &host_key, key_reference); + + let pool = SshConnectionPool::new(); + let cancellation = CancellationToken::new(); + + let output = exec_command( + &pool, + &endpoint, + &["repo".to_string(), "inspect".to_string()], + ExecLimits::default(), + &cancellation, + ) + .await + .unwrap(); + assert!(output.success()); + assert!(String::from_utf8_lossy(&output.stdout).contains("echo")); + + // A second call against the same endpoint must reuse the pooled + // connection rather than opening a new one. + exec_command( + &pool, + &endpoint, + &["repo".to_string(), "status".to_string()], + ExecLimits::default(), + &cancellation, + ) + .await + .unwrap(); + + assert_eq!(pool.pooled_connection_count().await, 1); + } + + #[tokio::test] + async fn exec_command_enforces_total_deadline() { + let (addr, host_key) = start_mock_server(1).await; + let client_key = test_host_key(); + let temp_dir = tempfile::tempdir().unwrap(); + let key_reference = write_client_key(temp_dir.path(), &client_key); + let endpoint = test_endpoint(addr, &host_key, key_reference); + + let pool = SshConnectionPool::new(); + let cancellation = CancellationToken::new(); + let limits = ExecLimits { + deadline: Duration::from_millis(200), + ..ExecLimits::default() + }; + + let error = exec_command( + &pool, + &endpoint, + &["repo".to_string(), "inspect".to_string()], + limits, + &cancellation, + ) + .await + .unwrap_err(); + assert_eq!(error, SshTransportError::DeadlineExceeded); + } + + #[tokio::test] + async fn exec_command_enforces_output_limit() { + let (addr, host_key) = start_mock_server(2).await; + let client_key = test_host_key(); + let temp_dir = tempfile::tempdir().unwrap(); + let key_reference = write_client_key(temp_dir.path(), &client_key); + let endpoint = test_endpoint(addr, &host_key, key_reference); + + let pool = SshConnectionPool::new(); + let cancellation = CancellationToken::new(); + let limits = ExecLimits { + deadline: Duration::from_secs(10), + max_output_bytes: 1024, + }; + + let error = exec_command( + &pool, + &endpoint, + &["repo".to_string(), "inspect".to_string()], + limits, + &cancellation, + ) + .await + .unwrap_err(); + assert_eq!(error, SshTransportError::OutputLimitExceeded); + } + + #[tokio::test] + async fn exec_command_is_cancellable() { + let (addr, host_key) = start_mock_server(1).await; + let client_key = test_host_key(); + let temp_dir = tempfile::tempdir().unwrap(); + let key_reference = write_client_key(temp_dir.path(), &client_key); + let endpoint = test_endpoint(addr, &host_key, key_reference); + + let pool = SshConnectionPool::new(); + let cancellation = CancellationToken::new(); + cancellation.cancel(); + + let error = exec_command( + &pool, + &endpoint, + &["repo".to_string(), "inspect".to_string()], + ExecLimits::default(), + &cancellation, + ) + .await + .unwrap_err(); + assert_eq!(error, SshTransportError::Cancelled); + } + + #[tokio::test] + async fn exec_command_rejects_unknown_host_key() { + let (addr, host_key) = start_mock_server(0).await; + let client_key = test_host_key(); + let temp_dir = tempfile::tempdir().unwrap(); + let key_reference = write_client_key(temp_dir.path(), &client_key); + let mut endpoint = test_endpoint(addr, &host_key, key_reference); + // Corrupt the trusted fingerprint so it no longer matches the server's + // real host key. + endpoint.host_keys[0].fingerprint_sha256 = "SHA256:not-the-real-key".to_string(); + + let pool = SshConnectionPool::new(); + let cancellation = CancellationToken::new(); + + let error = exec_command( + &pool, + &endpoint, + &["repo".to_string(), "inspect".to_string()], + ExecLimits::default(), + &cancellation, + ) + .await + .unwrap_err(); + assert!(matches!(error, SshTransportError::ConnectionFailed(_))); + } + + #[test] + fn build_remote_command_line_quotes_arguments() { + let line = build_remote_command_line(&[ + "repo".to_string(), + "inspect".to_string(), + "/srv/my app".to_string(), + ]); + assert_eq!(line, "treq 'repo' 'inspect' '/srv/my app'"); + } +}