diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index d3b212dd..395d554e 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fc36d7440d015b45468ba08a21e80e056ebbd014c611c6298bb7503e04bd1" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.1", + "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.8.12" @@ -84,6 +121,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", + "cpufeatures 0.3.1", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.8" @@ -265,6 +314,35 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" +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" @@ -289,6 +367,17 @@ 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.1", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -352,13 +441,22 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" +dependencies = [ + "digest 0.11.3", +] + [[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]] @@ -366,6 +464,16 @@ name = "block-buffer" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +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", ] @@ -392,6 +500,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.8.1" @@ -564,6 +682,15 @@ dependencies = [ "toml 1.2.1+spec-1.1.0", ] +[[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.11.2" @@ -571,6 +698,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ef9e3e8154a45c7a95cff4c1299702106ad94062d7bb85fdc5d3a5afd91d45" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -620,8 +749,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", + "cipher", "cpufeatures 0.3.1", "rand_core 0.10.1", + "zeroize", ] [[package]] @@ -631,11 +762,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout", + "zeroize", +] + +[[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" @@ -757,6 +911,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" @@ -844,13 +1004,30 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914c195c4d7fbc5ed7f88d14cbf85914d5c664967658d43404b263913a477e82" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.3", + "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", ] @@ -860,7 +1037,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "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]] @@ -908,6 +1097,15 @@ dependencies = [ "link-section", ] +[[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.8" @@ -915,6 +1113,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261af4dc7a06ea8a527a8717d8cbba3417530d6ea14e1edb3de6b67806be474f" 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.1", + "curve25519-dalek-derive", + "digest 0.11.3", + "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.119", ] [[package]] @@ -978,6 +1205,17 @@ dependencies = [ "zeroize", ] +[[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.119", +] + [[package]] name = "der" version = "0.7.10" @@ -985,7 +1223,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", - "pem-rfc7468", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a996a108feb8b427a6e12fcf6af4aa83409b79d72c15337d7741368131ee004" +dependencies = [ + "const-oid 0.10.3", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -1041,6 +1290,15 @@ dependencies = [ "syn 2.0.119", ] +[[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" @@ -1225,6 +1483,47 @@ 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 0.8.3", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8 0.11.0", + "signature 3.0.0", +] + +[[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.1", + "signature 3.0.0", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.19.0" @@ -1234,6 +1533,28 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900f37e7e18d74a2347389af6cd69e3a30d5cc9e697f40004865679aa75e9c7c" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff", + "group", + "hkdf 0.13.0", + "hybrid-array", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.12" @@ -1260,6 +1581,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.119", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1366,6 +1699,22 @@ dependencies = [ "simd-adler32", ] +[[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" @@ -1458,12 +1807,33 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.34" @@ -1550,6 +1920,7 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1669,6 +2040,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "generic-array" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1703,9 +2084,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "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]] @@ -1804,6 +2197,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" @@ -1930,6 +2334,12 @@ 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 = "hickory-net" version = "0.26.3" @@ -2009,6 +2419,15 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2102,7 +2521,10 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ + "ctutils", + "subtle", "typenum", + "zeroize", ] [[package]] @@ -2324,6 +2746,16 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a580c13f0ffc335ef42dd3f6b1a41b726051c5e1656d9a3aa6fddb3c5dfbbf9" +dependencies = [ + "block-padding", + "hybrid-array", +] + [[package]] name = "ipconfig" version = "0.3.4" @@ -2538,6 +2970,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.107" @@ -2571,6 +3013,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.1", +] + +[[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.8.3" @@ -2827,6 +3289,12 @@ dependencies = [ "digest 0.11.3", ] +[[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.3" @@ -2879,6 +3347,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ca78b66f46050d49fa21693ae8adab2081d8101b65d8067316b6b70b64a0d1" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha3", + "shake", +] + +[[package]] +name = "module-lattice" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e412c8ee83a1c678f427806219c49c96b2511b9f07497514f0e43fb26155" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "moka" version = "0.12.16" @@ -3051,6 +3545,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.10.4", + "rand_core 0.10.1", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -3367,6 +3873,67 @@ 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.1", +] + +[[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.1", +] + +[[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.1", +] + +[[package]] +name = "pageant" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bc922d9aa8cfcf598de27b3c6122cb2eeda9d6fe71527a22ebcb7f633540ff" +dependencies = [ + "base16ct", + "byteorder", + "bytes", + "delegate", + "futures", + "log", + "rand 0.10.4", + "sha2 0.11.1", + "thiserror 2.0.21", + "tokio", + "windows", + "windows-strings", +] + [[package]] name = "pango" version = "0.18.3" @@ -3421,6 +3988,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "phc", +] + [[package]] name = "pbkdf2" version = "0.13.0" @@ -3428,6 +4004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ "digest 0.11.3", + "hmac 0.13.0", ] [[package]] @@ -3439,12 +4016,31 @@ dependencies = [ "base64ct", ] +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[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.13.1" @@ -3574,9 +4170,36 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der 0.8.3", + "spki 0.8.1", +] + +[[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 0.8.3", + "pbkdf2", + "rand_core 0.10.1", + "scrypt", + "sha2 0.11.1", + "spki 0.8.1", ] [[package]] @@ -3585,8 +4208,20 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.3", + "pkcs5", + "rand_core 0.10.1", + "spki 0.8.1", ] [[package]] @@ -3641,6 +4276,29 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures 0.3.1", + "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.1", + "universal-hash", + "zeroize", +] + [[package]] name = "portable-atomic" version = "1.15.0" @@ -3697,6 +4355,33 @@ dependencies = [ "num-traits", ] +[[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" @@ -3781,8 +4466,11 @@ dependencies = [ "keyring", "mac_address", "mongodb", + "rand 0.10.4", + "rand_core 0.6.4", "redis", "rusqlite", + "russh", "rust_decimal", "serde", "serde_json", @@ -3947,7 +4635,7 @@ dependencies = [ "futures-util", "itertools", "itoa", - "num-bigint", + "num-bigint 0.4.8", "percent-encoding", "pin-project-lite", "ryu", @@ -4086,6 +4774,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac 0.13.0", +] + [[package]] name = "rfd" version = "0.17.2" @@ -4137,15 +4835,34 @@ dependencies = [ "num-bigint-dig", "num-integer", "num-traits", - "pkcs1", - "pkcs8", + "pkcs1 0.7.5", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] +[[package]] +name = "rsa" +version = "0.10.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" +dependencies = [ + "const-oid 0.10.3", + "crypto-bigint", + "crypto-primes", + "digest 0.11.3", + "pkcs1 0.8.0-rc.4", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha2 0.11.1", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -4160,6 +4877,101 @@ dependencies = [ "smallvec", ] +[[package]] +name = "russh" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94be3d86c71b9ed220a675c405e35c7a02565a009bf8da2e9cc13a700320a5c5" +dependencies = [ + "aes", + "aws-lc-rs", + "bitflags 2.13.2", + "block-padding", + "byteorder", + "bytes", + "cbc", + "cipher", + "crypto-bigint", + "ctr", + "curve25519-dalek", + "data-encoding", + "delegate", + "der 0.8.3", + "digest 0.11.3", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "flate2", + "futures", + "generic-array 1.4.5", + "getrandom 0.4.3", + "ghash", + "hex-literal", + "hmac 0.13.0", + "inout", + "keccak", + "log", + "md5", + "ml-kem", + "module-lattice", + "num-bigint 0.5.1", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2", + "pkcs1 0.8.0-rc.4", + "pkcs5", + "pkcs8 0.11.0", + "polyval", + "rand 0.10.4", + "rand_core 0.10.1", + "rsa 0.10.0-rc.18", + "russh-cryptovec", + "russh-util", + "salsa20", + "scrypt", + "sec1", + "sha1 0.11.0", + "sha2 0.11.1", + "sha3", + "signature 3.0.0", + "spki 0.8.1", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror 2.0.21", + "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", + "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_decimal" version = "1.43.0" @@ -4262,6 +5074,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" @@ -4328,6 +5150,32 @@ 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.1", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der 0.8.3", + "hybrid-array", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4562,6 +5410,16 @@ dependencies = [ "syn 3.0.10", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -4652,6 +5510,28 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak", + "sponge-cursor", +] + [[package]] name = "shlex" version = "2.0.1" @@ -4678,6 +5558,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -4805,9 +5695,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.3", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlparser" version = "0.64.0" @@ -4929,9 +5835,9 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array", + "generic-array 0.14.7", "hex", - "hkdf", + "hkdf 0.12.4", "hmac 0.12.1", "itoa", "log", @@ -4940,7 +5846,7 @@ dependencies = [ "once_cell", "percent-encoding", "rand 0.8.8", - "rsa", + "rsa 0.9.10", "rust_decimal", "serde", "sha1 0.10.7", @@ -4973,7 +5879,7 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf", + "hkdf 0.12.4", "hmac 0.12.1", "home", "ipnetwork", @@ -5023,6 +5929,65 @@ dependencies = [ "uuid", ] +[[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.3", + "pem-rfc7468 1.0.0", + "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 0.13.0", + "p256", + "p384", + "p521", + "rand_core 0.10.1", + "rsa 0.10.0-rc.18", + "sec1", + "sha1 0.11.0", + "sha2 0.11.1", + "signature 3.0.0", + "ssh-cipher", + "ssh-encoding", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6043,6 +7008,16 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[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 = "untrusted" version = "0.9.0" @@ -6894,6 +7869,18 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff", + "group", + "hybrid-array", + "primefield", +] + [[package]] name = "writeable" version = "0.6.4" diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 4e709356..f2950422 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ serde_json = "1" uuid = { version = "1", features = ["v4", "serde"] } rusqlite = { version = "0.32", features = ["bundled"] } dirs = "5" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net", "io-util", "sync"] } sqlx = { version = "0.8", default-features = false, features = [ "runtime-tokio", "postgres", @@ -45,6 +45,8 @@ mac_address = "1.1" bit-vec = "0.6.3" redis = { version = "0.27", features = ["aio", "tokio-comp"] } mongodb = "3.10.1" +russh = { version = "0.65.1", features = ["default"] } +rand = "0.10.4" [profile.release] codegen-units = 1 diff --git a/apps/tauri/src-tauri/src/commands.rs b/apps/tauri/src-tauri/src/commands.rs index db01fcf8..a35a54b8 100644 --- a/apps/tauri/src-tauri/src/commands.rs +++ b/apps/tauri/src-tauri/src/commands.rs @@ -914,3 +914,27 @@ pub fn db_apply_staging( ) -> Result { apply_staging_request(®istry, request) } + +/// Open a local SSH forward (`ssh -L`) for SQL drivers behind a bastion (#1894). +#[tauri::command] +pub fn ssh_tunnel_open( + request: crate::ssh_tunnel::SshTunnelOpenRequest, + tunnels: State<'_, crate::ssh_tunnel::SshTunnelRegistry>, +) -> Result { + tunnels.open(request) +} + +#[tauri::command] +pub fn ssh_tunnel_close( + id: String, + tunnels: State<'_, crate::ssh_tunnel::SshTunnelRegistry>, +) -> Result { + tunnels.close(&id) +} + +#[tauri::command] +pub fn ssh_tunnel_list( + tunnels: State<'_, crate::ssh_tunnel::SshTunnelRegistry>, +) -> Result, crate::ssh_tunnel::SshTunnelError> { + tunnels.list() +} diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index f519b7a6..a67b2414 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ mod commands; mod connection; mod profiles; mod secrets; +mod ssh_tunnel; mod staging; pub use connection::{ @@ -12,6 +13,9 @@ pub use connection::{ }; pub use profiles::{ConnectionProfile, ProfileStore, ProfileStoreError}; pub use secrets::{KeyringSecretStore, MemorySecretStore, SecretError, SecretStore, SharedSecretStore}; +pub use ssh_tunnel::{ + SshTunnelError, SshTunnelInfo, SshTunnelOpenRequest, SshTunnelRegistry, +}; pub use staging::{ apply_plan_mysql, apply_plan_postgres, apply_plan_sqlite, apply_staging_mysql, apply_staging_postgres, apply_staging_sqlite, generate_plan, ApplyError, ApplyResult, @@ -27,7 +31,7 @@ use commands::{ mongo_list_databases, mongo_ping, profiles_connect_mongo, profiles_connect_mysql, profiles_connect_postgres, profiles_connect_redis, profiles_connect_sqlite, profiles_delete, profiles_list, profiles_upsert, redis_get, redis_key_metas, redis_ping, redis_read_key, - redis_scan, redis_set, + redis_scan, redis_set, ssh_tunnel_close, ssh_tunnel_list, ssh_tunnel_open, }; #[tauri::command] @@ -81,6 +85,7 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .manage(ConnectionRegistry::new()) .manage(KeyringSecretStore::shared()) + .manage(SshTunnelRegistry::new()) .setup(|app| { use tauri::Manager; let store = ProfileStore::open_default().map_err(|e| { @@ -130,6 +135,9 @@ pub fn run() { profiles_connect_mysql, profiles_connect_redis, profiles_connect_mongo, + ssh_tunnel_open, + ssh_tunnel_close, + ssh_tunnel_list, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/tauri/src-tauri/src/ssh_tunnel.rs b/apps/tauri/src-tauri/src/ssh_tunnel.rs new file mode 100644 index 00000000..f37e1fd2 --- /dev/null +++ b/apps/tauri/src-tauri/src/ssh_tunnel.rs @@ -0,0 +1,632 @@ +//! Local SSH port forwarding via `russh` (`ssh -L` parity for SQL drivers). + +use russh::client::{self, Config}; +use russh::keys::{decode_secret_key, load_secret_key, PrivateKeyWithHashAlg, PublicKeyOrCertificate}; +use russh::{ChannelMsg, Disconnect}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::task::JoinHandle; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SshTunnelOpenRequest { + pub bastion_host: String, + pub bastion_port: u16, + pub username: String, + #[serde(default)] + pub password: Option, + /// PEM / OpenSSH private key contents (session-only). + #[serde(default)] + pub private_key_pem: Option, + /// Path to a private key file on disk. + #[serde(default)] + pub private_key_path: Option, + #[serde(default)] + pub private_key_passphrase: Option, + /// Target host as seen from the bastion (often a private IP / `localhost`). + pub remote_host: String, + pub remote_port: u16, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SshTunnelInfo { + pub id: String, + pub local_host: String, + pub local_port: u16, + pub remote_host: String, + pub remote_port: u16, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SshTunnelError { + pub message: String, +} + +impl SshTunnelError { + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl std::fmt::Display for SshTunnelError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for SshTunnelError {} + +struct ClientHandler; + +impl client::Handler for ClientHandler { + type Error = russh::Error; + + async fn check_server_key( + &mut self, + _server_public_key: &PublicKeyOrCertificate, + ) -> Result { + // TOFU for the first Tauri slice — fingerprint pinning lands with secrets UX. + Ok(true) + } +} + +struct LiveTunnel { + info: SshTunnelInfo, + stop: Arc, + accept_task: JoinHandle<()>, + session: Arc>, +} + +/// In-memory tunnel registry managed by Tauri. +#[derive(Default)] +pub struct SshTunnelRegistry { + inner: Mutex>, +} + +impl SshTunnelRegistry { + pub fn new() -> Self { + Self::default() + } + + pub fn open(&self, request: SshTunnelOpenRequest) -> Result { + crate::connection::runtime::block_on(self.open_async(request)) + } + + async fn open_async( + &self, + request: SshTunnelOpenRequest, + ) -> Result { + validate(&request)?; + let (info, live) = open_tunnel(request).await?; + let mut guard = self + .inner + .lock() + .map_err(|_| SshTunnelError::new("ssh tunnel registry lock poisoned"))?; + guard.insert(info.id.clone(), live); + Ok(info) + } + + pub fn close(&self, id: &str) -> Result { + let mut guard = self + .inner + .lock() + .map_err(|_| SshTunnelError::new("ssh tunnel registry lock poisoned"))?; + let Some(live) = guard.remove(id) else { + return Ok(false); + }; + live.stop.store(true, Ordering::SeqCst); + live.accept_task.abort(); + let session = live.session; + // Disconnect off the caller's thread so `close` is safe from inside + // `runtime::block_on` (nested block_on panics). + std::thread::spawn(move || { + crate::connection::runtime::block_on(async move { + let _ = session + .disconnect(Disconnect::ByApplication, "tunnel closed", "en") + .await; + }); + }); + Ok(true) + } + + pub fn list(&self) -> Result, SshTunnelError> { + let guard = self + .inner + .lock() + .map_err(|_| SshTunnelError::new("ssh tunnel registry lock poisoned"))?; + Ok(guard.values().map(|t| t.info.clone()).collect()) + } +} + +fn validate(req: &SshTunnelOpenRequest) -> Result<(), SshTunnelError> { + if req.bastion_host.trim().is_empty() { + return Err(SshTunnelError::new("bastion host is empty")); + } + if req.bastion_port == 0 { + return Err(SshTunnelError::new("bastion port is 0")); + } + if req.username.trim().is_empty() { + return Err(SshTunnelError::new("SSH username is empty")); + } + if req.remote_host.trim().is_empty() { + return Err(SshTunnelError::new("remote host is empty")); + } + if req.remote_port == 0 { + return Err(SshTunnelError::new("remote port is 0")); + } + let has_password = req + .password + .as_deref() + .map(str::trim) + .is_some_and(|s| !s.is_empty()); + let has_pem = req + .private_key_pem + .as_deref() + .map(str::trim) + .is_some_and(|s| !s.is_empty()); + let has_path = req + .private_key_path + .as_deref() + .map(str::trim) + .is_some_and(|s| !s.is_empty()); + if !has_password && !has_pem && !has_path { + return Err(SshTunnelError::new( + "SSH auth requires a password or private key", + )); + } + Ok(()) +} + +async fn open_tunnel( + request: SshTunnelOpenRequest, +) -> Result<(SshTunnelInfo, LiveTunnel), SshTunnelError> { + let config = Arc::new(Config { + nodelay: true, + inactivity_timeout: Some(Duration::from_secs(300)), + ..Default::default() + }); + + let addrs = ( + request.bastion_host.trim().to_owned(), + request.bastion_port, + ); + let mut session = client::connect(config, addrs, ClientHandler) + .await + .map_err(|e| SshTunnelError::new(format!("SSH connect failed: {e}")))?; + + authenticate(&mut session, &request).await?; + + let listener = TcpListener::bind(("127.0.0.1", 0)) + .await + .map_err(|e| SshTunnelError::new(format!("bind local forward failed: {e}")))?; + let local_addr = listener + .local_addr() + .map_err(|e| SshTunnelError::new(format!("local addr: {e}")))?; + + let id = Uuid::new_v4().to_string(); + let info = SshTunnelInfo { + id: id.clone(), + local_host: "127.0.0.1".into(), + local_port: local_addr.port(), + remote_host: request.remote_host.trim().to_owned(), + remote_port: request.remote_port, + }; + + let stop = Arc::new(AtomicBool::new(false)); + let stop_flag = Arc::clone(&stop); + let session = Arc::new(session); + let handle = Arc::clone(&session); + let remote_host = info.remote_host.clone(); + let remote_port = info.remote_port; + + let accept_task = tokio::spawn(async move { + loop { + if stop_flag.load(Ordering::SeqCst) { + break; + } + let accept = tokio::time::timeout(Duration::from_millis(400), listener.accept()).await; + match accept { + Ok(Ok((socket, origin))) => { + let handle = Arc::clone(&handle); + let remote_host = remote_host.clone(); + tokio::spawn(async move { + if let Err(e) = + relay_one(handle, socket, origin, &remote_host, remote_port).await + { + eprintln!("ssh tunnel relay error: {e}"); + } + }); + } + Ok(Err(_)) => break, + Err(_) => continue, // accept timeout — recheck stop flag + } + } + }); + + let live = LiveTunnel { + info: info.clone(), + stop, + accept_task, + session, + }; + Ok((info, live)) +} + +async fn authenticate( + session: &mut client::Handle, + request: &SshTunnelOpenRequest, +) -> Result<(), SshTunnelError> { + let user = request.username.trim(); + + if let Some(path) = request + .private_key_path + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + { + let key = load_secret_key(Path::new(path), request.private_key_passphrase.as_deref()) + .map_err(|e| SshTunnelError::new(format!("load private key: {e}")))?; + return auth_publickey(session, user, key).await; + } + + if let Some(pem) = request + .private_key_pem + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + { + let key = decode_secret_key(pem, request.private_key_passphrase.as_deref()) + .map_err(|e| SshTunnelError::new(format!("parse private key: {e}")))?; + return auth_publickey(session, user, key).await; + } + + let password = request + .password + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + .ok_or_else(|| SshTunnelError::new("SSH password is empty"))?; + + let auth = session + .authenticate_password(user, password) + .await + .map_err(|e| SshTunnelError::new(format!("SSH password auth failed: {e}")))?; + if !auth.success() { + return Err(SshTunnelError::new("SSH password authentication rejected")); + } + Ok(()) +} + +async fn auth_publickey( + session: &mut client::Handle, + user: &str, + key: russh::keys::PrivateKey, +) -> Result<(), SshTunnelError> { + let hash = session + .best_supported_rsa_hash() + .await + .map_err(|e| SshTunnelError::new(format!("SSH key hash negotiate: {e}")))? + .flatten(); + let auth = session + .authenticate_publickey( + user, + PrivateKeyWithHashAlg::new(Arc::new(key), hash), + ) + .await + .map_err(|e| SshTunnelError::new(format!("SSH publickey auth failed: {e}")))?; + if !auth.success() { + return Err(SshTunnelError::new( + "SSH public key authentication rejected", + )); + } + Ok(()) +} + +async fn relay_one( + session: Arc>, + mut stream: TcpStream, + origin: std::net::SocketAddr, + remote_host: &str, + remote_port: u16, +) -> Result<(), SshTunnelError> { + let mut channel = session + .channel_open_direct_tcpip( + remote_host, + u32::from(remote_port), + origin.ip().to_string(), + u32::from(origin.port()), + ) + .await + .map_err(|e| SshTunnelError::new(format!("direct-tcpip open failed: {e}")))?; + + let mut stream_closed = false; + let mut buf = vec![0_u8; 65536]; + loop { + tokio::select! { + r = stream.read(&mut buf), if !stream_closed => { + match r { + Ok(0) => { + stream_closed = true; + let _ = channel.eof().await; + } + Ok(n) => { + channel + .data(&buf[..n]) + .await + .map_err(|e| SshTunnelError::new(format!("channel write: {e}")))?; + } + Err(e) => return Err(SshTunnelError::new(format!("local read: {e}"))), + } + } + Some(msg) = channel.wait() => { + match msg { + ChannelMsg::Data { ref data } => { + stream + .write_all(data) + .await + .map_err(|e| SshTunnelError::new(format!("local write: {e}")))?; + } + ChannelMsg::Eof => { + if !stream_closed { + let _ = channel.eof().await; + } + break; + } + ChannelMsg::WindowAdjusted { .. } => {} + ChannelMsg::Close => break, + _ => {} + } + } + else => break, + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use russh::server::{Auth, Handler as ServerHandler, Msg, Server as _, Session}; + use russh::{server, Channel, ChannelId}; + use tokio::io::AsyncWriteExt; + + #[test] + fn validate_rejects_empty_host() { + let err = validate(&SshTunnelOpenRequest { + bastion_host: " ".into(), + bastion_port: 22, + username: "u".into(), + password: Some("p".into()), + private_key_pem: None, + private_key_path: None, + private_key_passphrase: None, + remote_host: "db".into(), + remote_port: 5432, + }) + .unwrap_err(); + assert!(err.message.contains("bastion host")); + } + + #[test] + fn validate_requires_credentials() { + let err = validate(&SshTunnelOpenRequest { + bastion_host: "bastion".into(), + bastion_port: 22, + username: "u".into(), + password: None, + private_key_pem: None, + private_key_path: None, + private_key_passphrase: None, + remote_host: "db".into(), + remote_port: 5432, + }) + .unwrap_err(); + assert!(err.message.contains("password or private key")); + } + + #[derive(Clone)] + struct ForwardServer; + + impl server::Server for ForwardServer { + type Handler = Self; + fn new_client(&mut self, _: Option) -> Self { + self.clone() + } + } + + impl ServerHandler for ForwardServer { + type Error = russh::Error; + + async fn auth_password( + &mut self, + user: &str, + password: &str, + ) -> Result { + if user == "querya" && password == "tunnel" { + Ok(Auth::Accept) + } else { + Ok(Auth::reject()) + } + } + + async fn channel_open_direct_tcpip( + &mut self, + channel: Channel, + host_to_connect: &str, + port_to_connect: u32, + _originator_address: &str, + _originator_port: u32, + reply: server::ChannelOpenHandle, + _session: &mut Session, + ) -> Result<(), Self::Error> { + let target = format!("{host_to_connect}:{port_to_connect}"); + let mut remote = match TcpStream::connect(&target).await { + Ok(s) => s, + Err(e) => { + reply + .reject(russh::ChannelOpenFailure::ConnectFailed) + .await; + return Err(russh::Error::IO(std::io::Error::other(format!( + "forward connect {target}: {e}" + )))); + } + }; + reply.accept().await; + let mut chan = channel.into_stream(); + tokio::spawn(async move { + let _ = tokio::io::copy_bidirectional(&mut remote, &mut chan).await; + }); + Ok(()) + } + + async fn channel_open_session( + &mut self, + _channel: Channel, + reply: server::ChannelOpenHandle, + _session: &mut Session, + ) -> Result<(), Self::Error> { + reply.accept().await; + Ok(()) + } + + async fn data( + &mut self, + _channel: ChannelId, + _data: &[u8], + _session: &mut Session, + ) -> Result<(), Self::Error> { + Ok(()) + } + } + + async fn spawn_echo_backend() -> (u16, JoinHandle<()>) { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let task = tokio::spawn(async move { + loop { + let Ok((mut sock, _)) = listener.accept().await else { + break; + }; + tokio::spawn(async move { + let mut buf = [0u8; 1024]; + if let Ok(n) = sock.read(&mut buf).await { + if n > 0 { + let _ = sock.write_all(&buf[..n]).await; + } + } + }); + } + }); + (port, task) + } + + async fn spawn_ssh_bastion() -> (u16, JoinHandle<()>) { + let key = + russh::keys::PrivateKey::random(&mut rand::rng(), russh::keys::Algorithm::Ed25519) + .unwrap(); + let config = Arc::new(server::Config { + keys: vec![key], + auth_rejection_time: Duration::from_millis(10), + auth_rejection_time_initial: Some(Duration::from_millis(0)), + ..Default::default() + }); + let socket = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = socket.local_addr().unwrap().port(); + let task = tokio::spawn(async move { + let mut server = ForwardServer; + let _ = server.run_on_socket(config, &socket).await; + }); + // Give the accept loop a moment to start. + tokio::time::sleep(Duration::from_millis(20)).await; + (port, task) + } + + #[test] + fn live_password_forward_echo() { + if std::env::var_os("QUERYA_SKIP_SSH").is_some() { + eprintln!("skip ssh live test (QUERYA_SKIP_SSH)"); + return; + } + + crate::connection::runtime::block_on(async { + let (backend_port, backend_task) = spawn_echo_backend().await; + let (bastion_port, bastion_task) = spawn_ssh_bastion().await; + + // Give the bastion a beat to accept. + tokio::time::sleep(Duration::from_millis(50)).await; + + let registry = SshTunnelRegistry::new(); + let info = registry + .open_async(SshTunnelOpenRequest { + bastion_host: "127.0.0.1".into(), + bastion_port, + username: "querya".into(), + password: Some("tunnel".into()), + private_key_pem: None, + private_key_path: None, + private_key_passphrase: None, + remote_host: "127.0.0.1".into(), + remote_port: backend_port, + }) + .await + .expect("open tunnel"); + + let mut client = TcpStream::connect(("127.0.0.1", info.local_port)) + .await + .expect("connect local forward"); + client.write_all(b"ping-via-ssh").await.unwrap(); + let mut buf = [0u8; 32]; + let n = tokio::time::timeout(Duration::from_secs(3), client.read(&mut buf)) + .await + .expect("read timeout") + .expect("read"); + assert_eq!(&buf[..n], b"ping-via-ssh"); + + assert!(registry.close(&info.id).unwrap()); + backend_task.abort(); + bastion_task.abort(); + }); + } + + #[test] + fn open_rejects_bad_password() { + if std::env::var_os("QUERYA_SKIP_SSH").is_some() { + return; + } + crate::connection::runtime::block_on(async { + let (bastion_port, bastion_task) = spawn_ssh_bastion().await; + tokio::time::sleep(Duration::from_millis(50)).await; + let registry = SshTunnelRegistry::new(); + let err = registry + .open_async(SshTunnelOpenRequest { + bastion_host: "127.0.0.1".into(), + bastion_port, + username: "querya".into(), + password: Some("wrong".into()), + private_key_pem: None, + private_key_path: None, + private_key_passphrase: None, + remote_host: "127.0.0.1".into(), + remote_port: 9, + }) + .await + .unwrap_err(); + assert!( + err.message.contains("rejected") || err.message.contains("auth"), + "{err}" + ); + bastion_task.abort(); + }); + } +}