diff --git a/Cargo.lock b/Cargo.lock index 5e4a8d0..08e9f19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "bytes", "crypto-common 0.1.7", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -49,6 +49,7 @@ dependencies = [ "cipher 0.5.2", "cpubits", "cpufeatures 0.3.1", + "zeroize", ] [[package]] @@ -77,6 +78,7 @@ dependencies = [ "ctr 0.10.1", "ctutils", "ghash 0.6.0", + "zeroize", ] [[package]] @@ -134,12 +136,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", - "blake2", + "blake2 0.10.6", "cpufeatures 0.2.17", - "password-hash", + "password-hash 0.5.0", "zeroize", ] +[[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.1", + "password-hash 0.6.1", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -384,6 +398,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.22.1" @@ -415,6 +435,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bcrypt-pbkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" +dependencies = [ + "blowfish 0.10.0", + "pbkdf2", + "sha2 0.11.0", +] + [[package]] name = "bitfields" version = "1.0.3" @@ -466,13 +497,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.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]] @@ -482,6 +522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", + "zeroize", ] [[package]] @@ -490,7 +531,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array", + "generic-array 0.14.7", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", ] [[package]] @@ -590,6 +640,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "cc" version = "1.4.5" @@ -636,8 +695,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", + "cipher 0.5.2", "cpufeatures 0.3.1", "rand_core 0.10.1", + "zeroize", ] [[package]] @@ -682,6 +743,7 @@ dependencies = [ "block-buffer 0.12.1", "crypto-common 0.2.2", "inout 0.2.2", + "zeroize", ] [[package]] @@ -858,19 +920,36 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", ] +[[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.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "serdect 0.4.3", + "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", "rand_core 0.6.4", "typenum", ] @@ -886,6 +965,16 @@ dependencies = [ "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 0.7.5", + "rand_core 0.10.1", +] + [[package]] name = "ctr" version = "0.9.2" @@ -911,6 +1000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -923,7 +1013,24 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[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 0.3.0", + "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -946,13 +1053,13 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa" dependencies = [ - "crypto-bigint", - "elliptic-curve", - "pkcs8", + "crypto-bigint 0.5.5", + "elliptic-curve 0.13.8", + "pkcs8 0.10.2", "rand_core 0.6.4", "serdect 0.3.0", - "sha3", - "signature", + "sha3 0.10.9", + "signature 2.2.0", "subtle", "zeroize", ] @@ -992,13 +1099,33 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "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 = "dbl" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1019,8 +1146,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" dependencies = [ "aes 0.8.4", - "block-padding", - "cbc", + "block-padding 0.3.3", + "cbc 0.1.2", "dbus", "fastrand", "hkdf 0.12.4", @@ -1030,6 +1157,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" @@ -1037,7 +1175,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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -1115,6 +1264,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "digest" version = "0.10.7" @@ -1186,10 +1344,10 @@ dependencies = [ "digest 0.10.7", "num-bigint-dig", "num-traits", - "pkcs8", - "rfc6979", + "pkcs8 0.10.2", + "rfc6979 0.4.0", "sha2 0.10.9", - "signature", + "signature 2.2.0", "zeroize", ] @@ -1218,12 +1376,27 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der 0.8.2", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", ] [[package]] @@ -1232,8 +1405,18 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[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]] @@ -1242,8 +1425,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.9", @@ -1251,6 +1434,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", + "rand_core 0.10.1", + "serde", + "sha2 0.11.0", + "signature 3.0.0", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.18.0" @@ -1266,18 +1465,18 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", + "base16ct 0.2.0", "base64ct", - "crypto-bigint", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", - "generic-array", - "group", + "ff 0.13.1", + "generic-array 0.14.7", + "group 0.13.0", "hkdf 0.12.4", - "pem-rfc7468", - "pkcs8", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "serde_json", "serdect 0.2.0", "subtle", @@ -1285,6 +1484,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hkdf 0.13.0", + "hybrid-array", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" version = "0.4.2" @@ -1316,6 +1537,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" @@ -1401,12 +1634,28 @@ dependencies = [ "subtle", ] +[[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.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.12" @@ -1591,6 +1840,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +dependencies = [ + "generic-array 0.14.7", + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1635,6 +1895,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ "polyval 0.7.3", + "zeroize", +] + +[[package]] +name = "gloo-timers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1643,11 +1916,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "h2" version = "0.4.19" @@ -1667,6 +1951,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1711,6 +2001,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 = "hkdf" version = "0.12.4" @@ -1818,7 +2114,10 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ + "ctutils", + "subtle", "typenum", + "zeroize", ] [[package]] @@ -2040,8 +2339,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", - "generic-array", + "block-padding 0.3.3", + "generic-array 0.14.7", ] [[package]] @@ -2050,6 +2349,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ + "block-padding 0.4.2", "hybrid-array", ] @@ -2137,11 +2437,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", "sha2 0.10.9", - "signature", + "signature 2.2.0", ] [[package]] @@ -2153,6 +2453,26 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[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 = "keyring" version = "3.6.3" @@ -2319,6 +2639,12 @@ dependencies = [ "digest 0.10.7", ] +[[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" @@ -2371,6 +2697,31 @@ 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 0.11.0", + "rand_core 0.10.1", + "sha3 0.11.0", +] + +[[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 = "multer" version = "3.1.0" @@ -2405,9 +2756,12 @@ dependencies = [ "mime_guess", "proton-drive-rs", "proton-sdk", + "rand 0.10.2", "rand 0.8.8", "regex", "rpassword", + "russh", + "russh-sftp", "rust-embed", "serde", "serde_json", @@ -2438,6 +2792,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "8.0.0" @@ -2488,6 +2854,8 @@ checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" dependencies = [ "num-integer", "num-traits", + "rand 0.10.2", + "rand_core 0.10.1", ] [[package]] @@ -2629,38 +2997,99 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", "sha2 0.10.9", ] +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.0", +] + [[package]] name = "p384" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "fiat-crypto 0.3.0", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.0", +] + [[package]] name = "p521" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primeorder", + "base16ct 0.2.0", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", "rand_core 0.6.4", "sha2 0.10.9", ] +[[package]] +name = "p521" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" +dependencies = [ + "base16ct 1.0.0", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.0", +] + +[[package]] +name = "pageant" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8eab09a361a4afe0b1668be978cd97e4f052e927a92b0b608cf902965d49ce" +dependencies = [ + "base16ct 1.0.0", + "byteorder", + "bytes", + "delegate", + "futures", + "log", + "rand 0.10.2", + "sha2 0.11.0", + "thiserror 2.0.20", + "tokio", + "windows", + "windows-strings", +] + [[package]] name = "parking" version = "2.2.1" @@ -2701,6 +3130,25 @@ dependencies = [ "subtle", ] +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2710,6 +3158,15 @@ 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" @@ -2726,10 +3183,10 @@ dependencies = [ "aes 0.8.4", "aes-gcm 0.10.3", "aes-kw", - "argon2", + "argon2 0.5.3", "base64 0.22.1", "bitfields", - "block-padding", + "block-padding 0.3.3", "blowfish 0.9.1", "buffer-redux", "byteorder", @@ -2741,19 +3198,19 @@ dependencies = [ "cipher 0.4.4", "const-oid 0.9.6", "crc24", - "curve25519-dalek", + "curve25519-dalek 4.1.3", "cx448", "derive_builder", "derive_more", - "des", + "des 0.8.1", "digest 0.10.7", "dsa", "eax", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", + "ecdsa 0.16.9", + "ed25519-dalek 2.2.0", + "elliptic-curve 0.13.8", "flate2", - "generic-array", + "generic-array 0.14.7", "hex", "hkdf 0.12.4", "idea", @@ -2766,18 +3223,18 @@ dependencies = [ "num-traits", "num_enum", "ocb3", - "p256", - "p384", - "p521", + "p256 0.13.2", + "p384 0.13.1", + "p521 0.13.3", "rand 0.8.8", "replace_with", "ripemd", - "rsa", + "rsa 0.9.10", "sha1 0.10.7", "sha1-checked", "sha2 0.10.9", - "sha3", - "signature", + "sha3 0.10.9", + "signature 2.2.0", "smallvec", "snafu", "subtle", @@ -2786,6 +3243,16 @@ dependencies = [ "zeroize", ] +[[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.12.1" @@ -2827,9 +3294,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.2", + "spki 0.8.0", +] + +[[package]] +name = "pkcs5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" +dependencies = [ + "aes 0.9.3", + "aes-gcm 0.11.1", + "cbc 0.2.1", + "der 0.8.2", + "pbkdf2", + "rand_core 0.10.1", + "scrypt", + "sha2 0.11.0", + "spki 0.8.0", ] [[package]] @@ -2838,8 +3332,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.2", + "pkcs5", + "rand_core 0.10.1", + "spki 0.8.0", ] [[package]] @@ -2868,6 +3374,17 @@ 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 0.6.1", + "zeroize", +] + [[package]] name = "polyval" version = "0.6.2" @@ -2889,6 +3406,7 @@ dependencies = [ "cpubits", "cpufeatures 0.3.1", "universal-hash 0.6.1", + "zeroize", ] [[package]] @@ -2909,13 +3427,40 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve 0.14.1", + "once_cell", + "primefield", + "serdect 0.4.3", + "wnaf", ] [[package]] @@ -3253,6 +3798,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -3298,15 +3853,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.2", + "crypto-bigint 0.7.5", + "crypto-primes", + "digest 0.11.3", + "pkcs1 0.8.0-rc.4", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha2 0.11.0", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + [[package]] name = "rtoolbox" version = "0.0.6" @@ -3317,6 +3891,120 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "russh" +version = "0.63.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "036204edbd199552a5b3832f63c60dcdf395dc44c7f06b4af1c0e8139cc11bce" +dependencies = [ + "aes 0.9.3", + "bitflags", + "block-padding 0.4.2", + "byteorder", + "bytes", + "cbc 0.2.1", + "cipher 0.5.2", + "crypto-bigint 0.7.5", + "ctr 0.10.1", + "curve25519-dalek 5.0.0", + "data-encoding", + "delegate", + "der 0.8.2", + "digest 0.11.3", + "ecdsa 0.17.0", + "ed25519-dalek 3.0.0", + "elliptic-curve 0.14.1", + "enum_dispatch", + "futures", + "generic-array 1.4.5", + "getrandom 0.4.3", + "ghash 0.6.0", + "hex-literal", + "hmac 0.13.0", + "inout 0.2.2", + "keccak 0.2.2", + "log", + "md5", + "ml-kem", + "module-lattice", + "num-bigint 0.5.1", + "p256 0.14.0", + "p384 0.14.0", + "p521 0.14.0", + "pageant", + "pbkdf2", + "pkcs1 0.8.0-rc.4", + "pkcs5", + "pkcs8 0.11.0", + "polyval 0.7.3", + "rand 0.10.2", + "rand_core 0.10.1", + "ring", + "rsa 0.10.0-rc.18", + "russh-cryptovec", + "russh-util", + "salsa20", + "scrypt", + "sec1 0.8.1", + "sha1 0.11.0", + "sha2 0.11.0", + "sha3 0.12.0", + "signature 3.0.0", + "spki 0.8.0", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror 2.0.20", + "tokio", + "typenum", + "universal-hash 0.6.1", + "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-sftp" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093197e526668d92bba562e2bbbe98d1af9831bf080b619c736316ca1fa35101" +dependencies = [ + "bitflags", + "bytes", + "chrono", + "dashmap", + "gloo-timers", + "log", + "serde", + "serde_bytes", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "wasm-bindgen-futures", +] + +[[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-embed" version = "8.12.0" @@ -3470,6 +4158,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 0.5.2", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3494,21 +4192,47 @@ 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 = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", + "base16ct 0.2.0", + "der 0.7.10", + "generic-array 0.14.7", + "pkcs8 0.10.2", "serdect 0.2.0", "subtle", "zeroize", ] +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.2", + "hybrid-array", + "subtle", + "zeroize", +] + [[package]] name = "secret-service" version = "4.0.0" @@ -3516,9 +4240,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" dependencies = [ "aes 0.8.4", - "cbc", + "cbc 0.1.2", "futures-util", - "generic-array", + "generic-array 0.14.7", "hkdf 0.12.4", "num", "once_cell", @@ -3567,6 +4291,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -3640,7 +4374,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "base16ct", + "base16ct 0.2.0", "serde", ] @@ -3650,7 +4384,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" dependencies = [ - "base16ct", + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", "serde", ] @@ -3716,7 +4460,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", - "keccak", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.2", +] + +[[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 0.2.2", + "sponge-cursor", ] [[package]] @@ -3763,6 +4528,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" @@ -3837,9 +4612,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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.2", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlx" version = "0.8.6" @@ -3942,7 +4733,7 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array", + "generic-array 0.14.7", "hex", "hkdf 0.12.4", "hmac 0.12.1", @@ -3953,7 +4744,7 @@ dependencies = [ "once_cell", "percent-encoding", "rand 0.8.8", - "rsa", + "rsa 0.9.10", "sha1 0.10.7", "sha2 0.10.9", "smallvec", @@ -4025,6 +4816,65 @@ dependencies = [ "url", ] +[[package]] +name = "ssh-cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" +dependencies = [ + "aead 0.6.1", + "aes 0.9.3", + "aes-gcm 0.11.1", + "chacha20", + "cipher 0.5.2", + "ctutils", + "des 0.9.0", + "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 0.7.5", + "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 0.6.0", + "bcrypt-pbkdf", + "ctutils", + "ed25519-dalek 3.0.0", + "hex", + "hmac 0.13.0", + "p256 0.14.0", + "p384 0.14.0", + "p521 0.14.0", + "rand_core 0.10.1", + "rsa 0.10.0-rc.18", + "sec1 0.8.1", + "sha1 0.11.0", + "sha2 0.11.0", + "signature 3.0.0", + "ssh-cipher", + "ssh-encoding", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4252,6 +5102,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "libc", "pin-project-lite", "tokio", @@ -4707,6 +5558,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4720,6 +5592,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4748,6 +5631,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4857,6 +5750,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4998,6 +5900,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "primefield", +] + [[package]] name = "writeable" version = "0.6.4" @@ -5019,7 +5933,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", "serde", "zeroize", @@ -5074,7 +5988,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "rand 0.8.8", "serde", diff --git a/Cargo.toml b/Cargo.toml index e305d66..47bbe61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,15 @@ proton-sdk = "0.6.2" rpassword = "7" hex = "0.4" +# SFTP mount: spaces and folders as a filesystem, authenticated by pubkey +# instead of the cookie session. `ring` rather than the default `aws-lc-rs` +# backend, because rustls (via lettre) already pulls `ring` in. +russh = { version = "0.63", default-features = false, features = ["ring", "rsa"] } +russh-sftp = "3" +# Only for generating the host key at first boot — ssh-key needs an rng that +# speaks rand_core 0.10, one major version past the `rand` used everywhere else. +rand10 = { package = "rand", version = "0.10" } + [features] default = [] # Keep the Proton session in the OS keyring instead of a 0600 file. Needs a diff --git a/migrations/0009_ssh_keys.sql b/migrations/0009_ssh_keys.sql new file mode 100644 index 0000000..b16de63 --- /dev/null +++ b/migrations/0009_ssh_keys.sql @@ -0,0 +1,13 @@ +-- Public keys allowed to mount the workspace over SFTP. A single-user +-- application still wants more than one key: one per machine that mounts it, +-- so losing a laptop means revoking one row rather than a shared secret. +CREATE TABLE ssh_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + -- `ssh-ed25519 AAAA...`, comment stripped: the comment is not part of the + -- key and would otherwise make the same key look different twice. + public_key TEXT NOT NULL UNIQUE, + fingerprint TEXT NOT NULL UNIQUE, + created_at INTEGER NOT NULL, + last_used_at INTEGER +); diff --git a/src/auth.rs b/src/auth.rs index f1efc25..5a65379 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -100,7 +100,7 @@ impl FromRequestParts for Session { #[cfg(test)] mod tests { use super::{credentials_match, issue_token, verify_token}; - use crate::config::{BackupConfig, Config, MailConfig}; + use crate::config::{BackupConfig, Config, MailConfig, SftpConfig}; fn config(session_days: i64) -> Config { Config { @@ -128,6 +128,7 @@ mod tests { digest_at: None, kinds: Vec::new(), }, + sftp: SftpConfig { bind: None }, } } diff --git a/src/config.rs b/src/config.rs index 4684314..168b9bc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,25 @@ pub struct Config { pub secure_cookie: bool, pub backup: BackupConfig, pub mail: MailConfig, + pub sftp: SftpConfig, +} + +/// The SFTP mount. Dormant until `WORKSPACE_SFTP_BIND` is set — same shape as +/// the mail relay, because a workspace with no key registered yet should not +/// refuse to start over a feature nobody has turned on. +#[derive(Debug, Clone)] +pub struct SftpConfig { + pub bind: Option, +} + +impl SftpConfig { + fn from_env() -> Result { + let bind = var("SFTP_BIND") + .map(|v| v.parse()) + .transpose() + .context("WORKSPACE_SFTP_BIND must look like 127.0.0.1:2222")?; + Ok(Self { bind }) + } } /// Time of day in the configured timezone, as `HH:MM`. @@ -194,6 +213,7 @@ impl Config { secure_cookie, backup: BackupConfig::from_env()?, mail: MailConfig::from_env()?, + sftp: SftpConfig::from_env()?, }) } @@ -206,6 +226,12 @@ impl Config { pub fn proton_session_path(&self) -> PathBuf { self.data_dir.join("proton-session.json") } + + /// The SFTP server's own identity, generated once on first boot. Losing it + /// only costs a host-key warning on every client, never data. + pub fn sftp_host_key_path(&self) -> PathBuf { + self.data_dir.join("sftp_host_key") + } } impl MailConfig { diff --git a/src/main.rs b/src/main.rs index 0664b0d..602231a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod error; mod mail; mod notify; mod routes; +mod sftp; mod state; mod static_files; mod throttle; @@ -110,11 +111,21 @@ async fn main() -> Result<()> { let state = AppState { db, - config, + config: Arc::clone(&config), login_throttle: Arc::default(), backup, notify, }; + + if let Some(sftp_bind) = config.sftp.bind { + let sftp_state = state.clone(); + tokio::spawn(async move { + if let Err(e) = sftp::serve(sftp_state, sftp_bind).await { + tracing::error!("sftp server stopped: {e:#}"); + } + }); + } + serve(bind, max_upload, state).await } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 81f5afa..2d36490 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -6,6 +6,7 @@ pub mod import; pub mod media; pub mod session; pub mod share; +pub mod ssh_keys; use axum::{ routing::{delete, get, post, put}, @@ -41,4 +42,6 @@ pub fn api_router() -> Router { .route("/media/{id}", get(media::serve).delete(media::remove)) .route("/share/{token}/{key}", get(share::read)) .route("/share/{token}/{key}/media/{id}", get(share::serve_media)) + .route("/ssh-keys", get(ssh_keys::list).post(ssh_keys::add)) + .route("/ssh-keys/{id}", delete(ssh_keys::remove)) } diff --git a/src/routes/ssh_keys.rs b/src/routes/ssh_keys.rs new file mode 100644 index 0000000..b7bc1f7 --- /dev/null +++ b/src/routes/ssh_keys.rs @@ -0,0 +1,109 @@ +use axum::{ + extract::{Path, State}, + Json, +}; +use serde::{Deserialize, Serialize}; +use sqlx::Row; + +use crate::{ + auth::Session, + error::{AppError, AppResult}, + sftp::fingerprint_of, + state::AppState, +}; + +#[derive(Serialize)] +pub struct SshKeySummary { + pub id: i64, + pub name: String, + pub fingerprint: String, + pub created_at: i64, + pub last_used_at: Option, +} + +#[derive(Deserialize)] +pub struct AddInput { + pub name: String, + /// A line from `authorized_keys`: `ssh-ed25519 AAAA... comment`. The + /// comment is accepted but not kept — it is not part of the key, and + /// keeping it would make the same key compare unequal to itself. + pub public_key: String, +} + +pub async fn list(_: Session, State(state): State) -> AppResult>> { + let rows = sqlx::query( + "SELECT id, name, fingerprint, created_at, last_used_at FROM ssh_keys ORDER BY created_at", + ) + .fetch_all(&state.db) + .await?; + + Ok(Json( + rows.iter() + .map(|row| SshKeySummary { + id: row.get("id"), + name: row.get("name"), + fingerprint: row.get("fingerprint"), + created_at: row.get("created_at"), + last_used_at: row.get("last_used_at"), + }) + .collect(), + )) +} + +pub async fn add( + _: Session, + State(state): State, + Json(input): Json, +) -> AppResult> { + let name = input.name.trim(); + if name.is_empty() { + return Err(AppError::BadRequest("a key needs a name".into())); + } + + let (normalized, fingerprint) = fingerprint_of(&input.public_key) + .map_err(|e| AppError::BadRequest(format!("not a public key: {e}")))?; + + let now = crate::now(); + let id: i64 = sqlx::query( + "INSERT INTO ssh_keys (name, public_key, fingerprint, created_at) VALUES (?1, ?2, ?3, ?4) + RETURNING id", + ) + .bind(name) + .bind(&normalized) + .bind(&fingerprint) + .bind(now) + .fetch_one(&state.db) + .await + .map_err(|e| match e { + sqlx::Error::Database(db) if db.is_unique_violation() => { + AppError::BadRequest("that key is already registered".into()) + } + e => e.into(), + })? + .get("id"); + + Ok(Json(SshKeySummary { + id, + name: name.to_string(), + fingerprint, + created_at: now, + last_used_at: None, + })) +} + +pub async fn remove( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult> { + let affected = sqlx::query("DELETE FROM ssh_keys WHERE id = ?1") + .bind(id) + .execute(&state.db) + .await? + .rows_affected(); + + if affected == 0 { + return Err(AppError::NotFound); + } + Ok(Json(serde_json::json!({ "ok": true }))) +} diff --git a/src/sftp/fs.rs b/src/sftp/fs.rs new file mode 100644 index 0000000..436d5ba --- /dev/null +++ b/src/sftp/fs.rs @@ -0,0 +1,578 @@ +//! The virtual filesystem an SFTP client sees: `///.../.md`. +//! Directories are spaces and folders; the only files are documents, always +//! `.md`. There is no on-disk mirror — every call reads or writes `nodes` +//! directly, so a save here is a save in the browser too. + +use std::collections::HashMap; + +use russh_sftp::protocol::{ + File, FileAttributes, Handle, Name, OpenFlags, StatusCode, Version, +}; +use sqlx::Row; + +use crate::{routes::media, routes::nodes, state::AppState}; + +type SftpResult = Result; + +/// A node as this filesystem sees it: enough to answer `stat` and to list a +/// parent directory without a second query. +struct Entry { + id: i64, + kind: String, + slug: String, + updated_at: i64, + size: i64, +} + +fn filename(entry: &Entry) -> String { + if entry.kind == nodes::DOCUMENT { + format!("{}.md", entry.slug) + } else { + entry.slug.clone() + } +} + +fn dir_attrs(mtime: i64) -> FileAttributes { + let mut attrs = FileAttributes::empty(); + attrs.set_dir(true); + attrs.permissions = Some(0o755 | attrs.permissions.unwrap_or(0)); + attrs.mtime = Some(mtime as u32); + attrs.atime = Some(mtime as u32); + attrs.size = Some(0); + attrs +} + +fn file_attrs(size: u64, mtime: i64) -> FileAttributes { + let mut attrs = FileAttributes::empty(); + attrs.set_regular(true); + attrs.permissions = Some(0o644 | attrs.permissions.unwrap_or(0)); + attrs.mtime = Some(mtime as u32); + attrs.atime = Some(mtime as u32); + attrs.size = Some(size); + attrs +} + +fn attrs_of(entry: &Entry) -> FileAttributes { + if entry.kind == nodes::DOCUMENT { + file_attrs(entry.size as u64, entry.updated_at) + } else { + dir_attrs(entry.updated_at) + } +} + +/// A path split into segments, `/` and `.` collapsed away. `readlink`, +/// symlinks and anything OpenSSH probes for are simply not implemented — +/// there are none of those here. +fn segments(path: &str) -> Vec<&str> { + path.split('/').filter(|s| !s.is_empty() && *s != ".").collect() +} + +/// A document's slug is its filename without the trailing `.md`. A filename +/// that does not end in `.md` never resolves — this filesystem holds no other +/// kind of file yet. +fn doc_slug(filename: &str) -> Option<&str> { + filename.strip_suffix(".md").filter(|base| !base.is_empty()) +} + +impl Default for Entry { + fn default() -> Self { + Entry { + id: 0, + kind: nodes::SPACE.to_string(), + slug: String::new(), + updated_at: 0, + size: 0, + } + } +} + +enum OpenHandle { + /// Directory listing, already fetched: `readdir` just drains it. + Dir(Vec), + /// A document's body, fetched once at `open`. Read-only handles never + /// touch the database again after this. + Read(Vec), + /// A document being written. `id` is `None` for a file that does not + /// exist yet — it is only created in `nodes` when the handle closes, + /// so an editor's write-then-fsync-then-close never leaves a half + /// document with the wrong name behind. + Write { + id: Option, + parent_id: i64, + slug: String, + name: String, + buf: Vec, + }, +} + +pub struct SpaceFs { + state: AppState, + handles: HashMap, + next_handle: u64, +} + +impl SpaceFs { + pub fn new(state: AppState) -> Self { + Self { + state, + handles: HashMap::new(), + next_handle: 0, + } + } + + fn allocate(&mut self, handle: OpenHandle) -> String { + self.next_handle += 1; + let key = self.next_handle.to_string(); + self.handles.insert(key.clone(), handle); + key + } + + /// Resolve a path of directory segments to a node id, `None` meaning the + /// root — which holds the spaces and nothing else. + async fn resolve_dir(&self, segs: &[&str]) -> SftpResult> { + let mut current: Option = None; + for seg in segs { + let row = sqlx::query( + "SELECT id FROM nodes WHERE parent_id IS ?1 AND slug = ?2 AND kind <> 'document'", + ) + .bind(current) + .bind(seg) + .fetch_optional(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)? + .ok_or(StatusCode::NoSuchFile)?; + current = Some(row.get("id")); + } + Ok(current) + } + + /// Split a path into the directory that must already exist and the + /// trailing segment, still unresolved. + fn split_path(path: &str) -> (Vec<&str>, Option<&str>) { + let mut segs = segments(path); + let leaf = segs.pop(); + (segs, leaf) + } + + async fn entry_at(&self, path: &str) -> SftpResult { + let segs = segments(path); + if segs.is_empty() { + return Ok(Entry::default()); + } + let (dir, leaf) = Self::split_path(path); + let Some(leaf) = leaf else { + return Ok(Entry::default()); + }; + let parent = self.resolve_dir(&dir).await; + + // A directory: space or folder, matched by slug under its parent. + if let Ok(parent_id) = parent { + let dir_row = sqlx::query( + "SELECT id, kind, slug, updated_at FROM nodes + WHERE parent_id IS ?1 AND slug = ?2 AND kind <> 'document'", + ) + .bind(parent_id) + .bind(leaf) + .fetch_optional(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + if let Some(row) = dir_row { + return Ok(Entry { + id: row.get("id"), + kind: row.get("kind"), + slug: row.get("slug"), + updated_at: row.get("updated_at"), + size: 0, + }); + } + + if let Some(slug) = doc_slug(leaf) { + let doc_row = sqlx::query( + "SELECT id, slug, updated_at, length(body) AS size FROM nodes + WHERE parent_id IS ?1 AND slug = ?2 AND kind = 'document'", + ) + .bind(parent_id) + .bind(slug) + .fetch_optional(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + if let Some(row) = doc_row { + return Ok(Entry { + id: row.get("id"), + kind: nodes::DOCUMENT.to_string(), + slug: row.get("slug"), + updated_at: row.get("updated_at"), + size: row.get("size"), + }); + } + } + } + + Err(StatusCode::NoSuchFile) + } + + async fn children(&self, parent_id: Option) -> SftpResult> { + let rows = sqlx::query( + "SELECT id, kind, slug, updated_at, length(body) AS size FROM nodes + WHERE parent_id IS ?1 ORDER BY kind, slug", + ) + .bind(parent_id) + .fetch_all(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + + Ok(rows + .iter() + .map(|row| Entry { + id: row.get("id"), + kind: row.get("kind"), + slug: row.get("slug"), + updated_at: row.get("updated_at"), + size: row.try_get("size").unwrap_or(0), + }) + .collect()) + } +} + +impl russh_sftp::server::Handler for SpaceFs { + type Error = StatusCode; + + fn unimplemented(&self) -> Self::Error { + StatusCode::OpUnsupported + } + + async fn init( + &mut self, + _version: u32, + _extensions: HashMap, + ) -> SftpResult { + Ok(Version::new()) + } + + async fn realpath(&mut self, id: u32, path: String) -> SftpResult { + let clean = segments(&path).join("/"); + Ok(Name { + id, + files: vec![File::dummy(format!("/{clean}"))], + }) + } + + async fn lstat(&mut self, id: u32, path: String) -> SftpResult { + self.stat(id, path).await + } + + async fn stat(&mut self, id: u32, path: String) -> SftpResult { + let entry = self.entry_at(&path).await?; + Ok(russh_sftp::protocol::Attrs { + id, + attrs: attrs_of(&entry), + }) + } + + async fn fstat(&mut self, id: u32, handle: String) -> SftpResult { + match self.handles.get(&handle) { + Some(OpenHandle::Read(data)) => Ok(russh_sftp::protocol::Attrs { + id, + attrs: file_attrs(data.len() as u64, crate::now()), + }), + Some(OpenHandle::Write { buf, .. }) => Ok(russh_sftp::protocol::Attrs { + id, + attrs: file_attrs(buf.len() as u64, crate::now()), + }), + Some(OpenHandle::Dir(_)) => Ok(russh_sftp::protocol::Attrs { + id, + attrs: dir_attrs(crate::now()), + }), + None => Err(StatusCode::Failure), + } + } + + async fn opendir(&mut self, id: u32, path: String) -> SftpResult { + let segs = segments(&path); + let parent_id = self.resolve_dir(&segs).await?; + let entries = self.children(parent_id).await?; + let handle = self.allocate(OpenHandle::Dir(entries)); + let _ = id; + Ok(Handle { id, handle }) + } + + async fn readdir(&mut self, id: u32, handle: String) -> SftpResult { + match self.handles.get_mut(&handle) { + Some(OpenHandle::Dir(entries)) if !entries.is_empty() => { + let files = entries + .drain(..) + .map(|entry| File::new(filename(&entry), attrs_of(&entry))) + .collect(); + Ok(Name { id, files }) + } + Some(OpenHandle::Dir(_)) => Err(StatusCode::Eof), + _ => Err(StatusCode::Failure), + } + } + + async fn open( + &mut self, + id: u32, + filename: String, + pflags: OpenFlags, + _attrs: FileAttributes, + ) -> SftpResult { + let (dir, leaf) = Self::split_path(&filename); + let leaf = leaf.ok_or(StatusCode::PermissionDenied)?; + let slug = doc_slug(leaf).ok_or(StatusCode::PermissionDenied)?; + let parent_id = self.resolve_dir(&dir).await?; + + let existing = sqlx::query( + "SELECT id, body FROM nodes WHERE parent_id IS ?1 AND slug = ?2 AND kind = 'document'", + ) + .bind(parent_id) + .bind(slug) + .fetch_optional(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + + if pflags.contains(OpenFlags::WRITE) { + if existing.is_none() && !pflags.contains(OpenFlags::CREATE) { + return Err(StatusCode::NoSuchFile); + } + if existing.is_some() && pflags.contains(OpenFlags::EXCLUDE) { + return Err(StatusCode::Failure); + } + let Some(parent_id) = parent_id else { + // A document cannot live loose at the root — only spaces do. + return Err(StatusCode::PermissionDenied); + }; + let buf = match &existing { + Some(row) if !pflags.contains(OpenFlags::TRUNCATE) => { + row.get::("body").into_bytes() + } + _ => Vec::new(), + }; + let handle = self.allocate(OpenHandle::Write { + id: existing.map(|row| row.get("id")), + parent_id, + slug: slug.to_string(), + name: slug.to_string(), + buf, + }); + return Ok(Handle { id, handle }); + } + + let row = existing.ok_or(StatusCode::NoSuchFile)?; + let body: String = row.get("body"); + let handle = self.allocate(OpenHandle::Read(body.into_bytes())); + Ok(Handle { id, handle }) + } + + async fn read( + &mut self, + id: u32, + handle: String, + offset: u64, + len: u32, + ) -> SftpResult { + let data = match self.handles.get(&handle) { + Some(OpenHandle::Read(data)) => data, + _ => return Err(StatusCode::Failure), + }; + let offset = offset as usize; + if offset >= data.len() { + return Err(StatusCode::Eof); + } + let end = (offset + len as usize).min(data.len()); + Ok(russh_sftp::protocol::Data { + id, + data: data[offset..end].to_vec(), + }) + } + + async fn write( + &mut self, + id: u32, + handle: String, + offset: u64, + data: Vec, + ) -> SftpResult { + match self.handles.get_mut(&handle) { + Some(OpenHandle::Write { buf, .. }) => { + let offset = offset as usize; + if buf.len() < offset { + buf.resize(offset, 0); + } + let end = offset + data.len(); + if buf.len() < end { + buf.resize(end, 0); + } + buf[offset..end].copy_from_slice(&data); + Ok(ok_status(id)) + } + _ => Err(StatusCode::Failure), + } + } + + async fn close(&mut self, id: u32, handle: String) -> SftpResult { + let Some(open) = self.handles.remove(&handle) else { + return Err(StatusCode::Failure); + }; + + let OpenHandle::Write { id: node_id, parent_id, slug, name, buf } = open else { + return Ok(ok_status(id)); + }; + + let body = String::from_utf8(buf).map_err(|_| StatusCode::Failure)?; + let now = crate::now(); + + match node_id { + Some(node_id) => { + sqlx::query("UPDATE nodes SET body = ?1, updated_at = ?2 WHERE id = ?3") + .bind(&body) + .bind(now) + .bind(node_id) + .execute(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + media::link_to_node(&self.state.db, node_id, &body) + .await + .ok(); + } + None => { + let slug = nodes::unique_slug(&self.state.db, Some(parent_id), &slug, None) + .await + .map_err(|_| StatusCode::Failure)?; + let new_id: i64 = sqlx::query( + "INSERT INTO nodes (parent_id, kind, name, slug, body, created_at, updated_at) + VALUES (?1, 'document', ?2, ?3, ?4, ?5, ?5) RETURNING id", + ) + .bind(parent_id) + .bind(&name) + .bind(&slug) + .bind(&body) + .bind(now) + .fetch_one(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)? + .get("id"); + media::link_to_node(&self.state.db, new_id, &body).await.ok(); + } + } + + self.state.backup.signal(); + Ok(ok_status(id)) + } + + async fn remove(&mut self, id: u32, filename: String) -> SftpResult { + let entry = self.entry_at(&filename).await?; + if entry.kind != nodes::DOCUMENT { + return Err(StatusCode::PermissionDenied); + } + sqlx::query("DELETE FROM nodes WHERE id = ?1") + .bind(entry.id) + .execute(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + self.state.backup.signal(); + Ok(ok_status(id)) + } + + async fn mkdir( + &mut self, + id: u32, + path: String, + _attrs: FileAttributes, + ) -> SftpResult { + let (dir, leaf) = Self::split_path(&path); + let name = leaf.ok_or(StatusCode::PermissionDenied)?; + let Some(parent_id) = self.resolve_dir(&dir).await? else { + // A new space is made in the browser, not by mounting the root + // and mkdir-ing into it — the root has no `has_board` to ask about. + return Err(StatusCode::PermissionDenied); + }; + let slug = nodes::unique_slug(&self.state.db, Some(parent_id), &nodes::slugify(name), None) + .await + .map_err(|_| StatusCode::Failure)?; + let now = crate::now(); + sqlx::query( + "INSERT INTO nodes (parent_id, kind, name, slug, created_at, updated_at) + VALUES (?1, 'folder', ?2, ?3, ?4, ?4)", + ) + .bind(parent_id) + .bind(name) + .bind(&slug) + .bind(now) + .execute(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + self.state.backup.signal(); + Ok(ok_status(id)) + } + + async fn rmdir(&mut self, id: u32, path: String) -> SftpResult { + let entry = self.entry_at(&path).await?; + if entry.kind == nodes::DOCUMENT { + return Err(StatusCode::PermissionDenied); + } + let children = self.children(Some(entry.id)).await?; + if !children.is_empty() { + return Err(StatusCode::Failure); + } + sqlx::query("DELETE FROM nodes WHERE id = ?1") + .bind(entry.id) + .execute(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + self.state.backup.signal(); + Ok(ok_status(id)) + } + + async fn rename( + &mut self, + id: u32, + oldpath: String, + newpath: String, + ) -> SftpResult { + let entry = self.entry_at(&oldpath).await?; + let (new_dir, new_leaf) = Self::split_path(&newpath); + let new_name = match entry.kind.as_str() { + k if k == nodes::DOCUMENT => new_leaf + .and_then(doc_slug) + .ok_or(StatusCode::PermissionDenied)?, + _ => new_leaf.ok_or(StatusCode::PermissionDenied)?, + }; + let new_parent = self.resolve_dir(&new_dir).await?; + let Some(new_parent) = new_parent else { + return Err(StatusCode::PermissionDenied); + }; + let slug = nodes::unique_slug( + &self.state.db, + Some(new_parent), + &nodes::slugify(new_name), + Some(entry.id), + ) + .await + .map_err(|_| StatusCode::Failure)?; + + sqlx::query( + "UPDATE nodes SET parent_id = ?1, name = ?2, slug = ?3, updated_at = ?4 WHERE id = ?5", + ) + .bind(new_parent) + .bind(new_name) + .bind(&slug) + .bind(crate::now()) + .bind(entry.id) + .execute(&self.state.db) + .await + .map_err(|_| StatusCode::Failure)?; + + self.state.backup.signal(); + Ok(ok_status(id)) + } +} + +fn ok_status(id: u32) -> russh_sftp::protocol::Status { + russh_sftp::protocol::Status { + id, + status_code: StatusCode::Ok, + error_message: "Ok".to_string(), + language_tag: "en-US".to_string(), + } +} diff --git a/src/sftp/mod.rs b/src/sftp/mod.rs new file mode 100644 index 0000000..8510930 --- /dev/null +++ b/src/sftp/mod.rs @@ -0,0 +1,147 @@ +//! Spaces and folders, mounted over SFTP instead of the browser. A separate +//! authentication path on purpose: the cookie session is for one browser tab +//! at a time, and a mount is a standing thing a laptop keeps open for weeks. +//! Trust here comes from a public key registered in `ssh_keys`, not a login. + +mod fs; + +use std::{net::SocketAddr, sync::Arc}; + +use anyhow::{Context, Result}; +use russh::{ + keys::{Algorithm, PrivateKey, PublicKey}, + server::{ + Auth, ChannelOpenHandle, Config as ServerConfig, Handler as ServerHandler, Msg, + Server as _, Session, + }, + Channel, ChannelId, +}; +use sqlx::Row; + +use crate::state::AppState; + +/// Normalise an `authorized_keys` line to `algorithm base64` (no comment) and +/// its SHA-256 fingerprint, the two things stored for a registered key. +pub fn fingerprint_of(line: &str) -> Result<(String, String)> { + let key = PublicKey::from_openssh(line.trim())?; + let normalized = key.to_openssh()?; + let fingerprint = key.fingerprint(russh::keys::HashAlg::Sha256).to_string(); + Ok((normalized, fingerprint)) +} + +/// Load the host key from disk, generating and persisting one on first boot. +/// Losing this file only costs every client a host-key warning, never data, +/// so it lives next to the database rather than anywhere more precious. +fn load_or_create_host_key(path: &std::path::Path) -> Result { + if path.exists() { + return PrivateKey::read_openssh_file(path) + .with_context(|| format!("could not read {}", path.display())); + } + let key = PrivateKey::random(&mut rand10::rng(), Algorithm::Ed25519)?; + key.write_openssh_file(path, russh::keys::ssh_key::LineEnding::LF) + .with_context(|| format!("could not write {}", path.display()))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; + } + Ok(key) +} + +pub async fn serve(state: AppState, bind: SocketAddr) -> Result<()> { + let host_key = load_or_create_host_key(&state.config.sftp_host_key_path())?; + + let config = Arc::new(ServerConfig { + keys: vec![host_key], + ..Default::default() + }); + + let mut server = SftpServer { state }; + tracing::info!("sftp mount listening on {bind}"); + server.run_on_address(config, bind).await?; + Ok(()) +} + +#[derive(Clone)] +struct SftpServer { + state: AppState, +} + +impl russh::server::Server for SftpServer { + type Handler = SshSession; + + fn new_client(&mut self, _addr: Option) -> Self::Handler { + SshSession { + state: self.state.clone(), + channel: None, + } + } +} + +struct SshSession { + state: AppState, + /// The one channel a mount opens for its sftp subsystem. A second + /// `channel_open_session` would mean a shell or a second subsystem, + /// neither of which this server offers. + channel: Option>, +} + +impl ServerHandler for SshSession { + type Error = anyhow::Error; + + async fn auth_publickey( + &mut self, + _user: &str, + public_key: &PublicKey, + ) -> Result { + let fingerprint = public_key.fingerprint(russh::keys::HashAlg::Sha256).to_string(); + let matched = sqlx::query("SELECT id FROM ssh_keys WHERE fingerprint = ?1") + .bind(&fingerprint) + .fetch_optional(&self.state.db) + .await?; + + match matched { + Some(row) => { + let id: i64 = row.get("id"); + sqlx::query("UPDATE ssh_keys SET last_used_at = ?1 WHERE id = ?2") + .bind(crate::now()) + .bind(id) + .execute(&self.state.db) + .await?; + Ok(Auth::Accept) + } + None => Ok(Auth::reject()), + } + } + + async fn channel_open_session( + &mut self, + channel: Channel, + reply: ChannelOpenHandle, + _session: &mut Session, + ) -> Result<(), Self::Error> { + self.channel = Some(channel); + reply.accept().await; + Ok(()) + } + + async fn subsystem_request( + &mut self, + channel_id: ChannelId, + name: &str, + session: &mut Session, + ) -> Result<(), Self::Error> { + if name != "sftp" { + session.channel_failure(channel_id)?; + return Ok(()); + } + let Some(channel) = self.channel.take() else { + session.channel_failure(channel_id)?; + return Ok(()); + }; + session.channel_success(channel_id)?; + let handler = fs::SpaceFs::new(self.state.clone()); + tokio::spawn(russh_sftp::server::run(channel.into_stream(), handler)); + Ok(()) + } +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index b9c0c1e..304ef6a 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -30,6 +30,15 @@ export interface WorkspaceNode extends NodeSummary { path: Crumb[] } +/** A public key allowed to mount the workspace over SFTP. */ +export interface SshKey { + id: number + name: string + fingerprint: string + created_at: number + last_used_at: number | null +} + export interface SharedDocument { name: string body: string @@ -248,4 +257,11 @@ export const api = { readShared: (token: string, key: string) => request(`/share/${encodeURIComponent(token)}/${encodeURIComponent(key)}`), + + listSshKeys: () => request('/ssh-keys'), + + addSshKey: (name: string, public_key: string) => + request('/ssh-keys', { method: 'POST', body: body({ name, public_key }) }), + + removeSshKey: (id: number) => request<{ ok: true }>(`/ssh-keys/${id}`, { method: 'DELETE' }), } diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts index c932513..5707cfa 100644 --- a/web/src/lib/commands.ts +++ b/web/src/lib/commands.ts @@ -703,6 +703,36 @@ export const commands: CommandSpec[] = [ }), }, }, + { + name: 'sshkey', + args: '[add | rm ]', + group: 'view', + help: 'list, add or remove keys allowed to mount the workspace over sftp', + run: (arg) => + workspace.guard(async () => { + const trimmed = arg.trim() + if (trimmed === '' || trimmed === 'ls' || trimmed === 'list') { + const keys = await api.listSshKeys() + if (keys.length === 0) return workspace.say('no keys registered — :sshkey add ') + return workspace.say(keys.map((k) => `${k.name} ${k.fingerprint}`).join(' · ')) + } + const addMatch = /^add\s+(\S+)\s+(.+)$/.exec(trimmed) + if (addMatch) { + const [, name, key] = addMatch + const added = await api.addSshKey(name, key) + return workspace.say(`added ${added.name} (${added.fingerprint})`) + } + const rmMatch = /^rm\s+(\S+)$/.exec(trimmed) + if (rmMatch) { + const keys = await api.listSshKeys() + const target = keys.find((k) => k.name === rmMatch[1]) + if (!target) return workspace.say(`no key named ${rmMatch[1]}`, 'error') + await api.removeSshKey(target.id) + return workspace.say(`removed ${target.name}`) + } + workspace.say('usage: :sshkey [add | rm ]', 'error') + }), + }, { name: 'help', aliases: ['h'],