diff --git a/Cargo.lock b/Cargo.lock index 9d6d5d5..0dd79da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -108,6 +108,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -164,6 +179,30 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "assert_cmd" version = "2.1.2" @@ -179,6 +218,107 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -200,6 +340,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.22.1" @@ -227,6 +373,29 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -245,6 +414,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array 0.4.7", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "1.12.1" @@ -280,7 +467,7 @@ version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e985a566bdaae9a428a957d12b10c318d41b2afddb54cfbb764878059df636e" dependencies = [ - "embedded-io", + "embedded-io 0.6.1", ] [[package]] @@ -320,6 +507,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -390,6 +579,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "cipher" version = "0.3.0" @@ -405,7 +606,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -450,6 +651,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -472,6 +691,37 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -497,6 +747,21 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -545,6 +810,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "hybrid-array 0.4.7", +] + [[package]] name = "ctr" version = "0.7.0" @@ -586,7 +860,7 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto", + "fiat-crypto 0.2.9", "rand_core 0.6.4", "rustc_version", "serde", @@ -594,6 +868,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.10", + "fiat-crypto 0.3.0", + "rand_core 0.9.5", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek-derive" version = "0.1.1" @@ -618,6 +910,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -631,14 +958,31 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", - "pem-rfc7468", + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -662,6 +1006,66 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "difflib" version = "0.4.0" @@ -684,11 +1088,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", - "crypto-common", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa94b64bfc6549e6e4b5a3216f22593224174083da7a90db47e951c4fb31725" +dependencies = [ + "block-buffer 0.11.0", + "const-oid 0.10.2", + "crypto-common 0.2.0", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -700,19 +1127,51 @@ dependencies = [ "syn", ] +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" 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", "serdect", "signature 2.2.0", - "spki", + "spki 0.7.3", ] [[package]] @@ -730,11 +1189,22 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "serde", "signature 2.2.0", ] +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "pkcs8 0.11.0-rc.11", + "serde", + "signature 3.0.0-rc.10", +] + [[package]] name = "ed25519-dalek" version = "1.0.1" @@ -765,6 +1235,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek 5.0.0-pre.1", + "ed25519 3.0.0-rc.4", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "signature 3.0.0-rc.10", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -784,8 +1270,8 @@ dependencies = [ "generic-array", "group", "hkdf", - "pem-rfc7468", - "pkcs8", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "serdect", @@ -793,12 +1279,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + [[package]] name = "embedded-io" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -861,12 +1376,51 @@ 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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +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 = "futures" version = "0.3.32" @@ -882,6 +1436,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -915,6 +1482,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -955,6 +1535,21 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1005,6 +1600,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasip3", + "wasm-bindgen", +] + [[package]] name = "ghash" version = "0.4.4" @@ -1031,6 +1641,18 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.13.0" @@ -1042,6 +1664,34 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1051,11 +1701,25 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -1066,12 +1730,79 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "http", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -1136,6 +1867,51 @@ dependencies = [ "x25519-dalek-ng", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hybrid-array" version = "0.2.3" @@ -1145,6 +1921,243 @@ dependencies = [ "typenum", ] +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.2", + "tokio", + "url", + "xmltree", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -1153,6 +2166,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1164,6 +2179,241 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "iroh" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5236da4d5681f317ec393c8fe2b7e3d360d31c6bb40383991d0b7429ca5ad117" +dependencies = [ + "backon", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "ed25519-dalek 3.0.0-pre.1", + "futures-util", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "igd-next", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "iroh-quinn-udp", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netwatch", + "papaya", + "pin-project", + "pkarr", + "pkcs8 0.11.0-rc.11", + "portmapper", + "rand 0.9.2", + "reqwest 0.12.28", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "smallvec", + "strum", + "sync_wrapper", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots", +] + +[[package]] +name = "iroh-base" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c99d836a1c99e037e98d1bf3ef209c3a4df97555a00ce9510eb78eccdf5567" +dependencies = [ + "curve25519-dalek 5.0.0-pre.1", + "data-encoding", + "derive_more", + "digest 0.11.0-rc.10", + "ed25519-dalek 3.0.0-pre.1", + "n0-error", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-metrics" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c946095f060e6e59b9ff30cc26c75cdb758e7fb0cde8312c89e2144654989fcb" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "postcard", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab063c2bfd6c3d5a33a913d4fdb5252f140db29ec67c704f20f3da7e8f92dbf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iroh-quinn" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034ed21f34c657a123d39525d948c885aacba59508805e4dd67d71f022e7151b" +dependencies = [ + "bytes", + "cfg_aliases", + "iroh-quinn-proto", + "iroh-quinn-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-proto" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de99ad8adc878ee0e68509ad256152ce23b8bbe45f5539d04e179630aca40a9" +dependencies = [ + "bytes", + "derive_more", + "enum-assoc", + "fastbloom", + "getrandom 0.3.4", + "identity-hash", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-udp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f981dadd5a072a9e0efcd24bdcc388e570073f7e51b33505ceb1ef4668c80c86" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "iroh-relay" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2b63e654b9dec799a73372cdc79b529ca6c7248c0c8de7da78a02e3a46f03c" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "lru", + "n0-error", + "n0-future", + "num_enum", + "pin-project", + "pkarr", + "postcard", + "rand 0.9.2", + "reqwest 0.12.28", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots", + "ws_stream_wasm", + "z32", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1198,6 +2448,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1233,6 +2493,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.182" @@ -1262,6 +2528,18 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1277,12 +2555,40 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "matchers" version = "0.2.0" @@ -1324,12 +2630,215 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" dependencies = [ - "hybrid-array", + "hybrid-array 0.2.3", "kem", "rand_core 0.6.4", "sha3", ] +[[package]] +name = "moka" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "n0-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38795f7932e6e9d1c6e989270ef5b3ff24ebb910e2c9d4bed2d28d8bae3007dc" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + +[[package]] +name = "netdev" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "libc", + "mac-addr", + "netlink-packet-core", + "netlink-packet-route 0.25.1", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454b8c0759b2097581f25ed5180b4a1d14c324fde6d0734932a288e044d06232" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "iroh-quinn-udp", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2 0.6.2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows", + "windows-result", + "wmi", +] + +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.17", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1345,6 +2854,99 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "object" version = "0.37.3" @@ -1359,6 +2961,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -1378,6 +2984,7 @@ version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded22991b43cd15561b62b2e1cf9ace1344a8534eebec96202d5c96a77a6616a" dependencies = [ + "argon2", "curve25519-dalek 4.1.3", "derive-where", "digest 0.10.7", @@ -1486,6 +3093,22 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1509,6 +3132,23 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem" version = "3.0.6" @@ -1528,20 +3168,112 @@ 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 = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkarr" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f950360d31be432c0c9467fba5024a94f55128e7f32bc9d32db140369f24c77" +dependencies = [ + "async-compat", + "base32", + "bytes", + "cfg_aliases", + "document-features", + "dyn-clone", + "ed25519-dalek 3.0.0-pre.1", + "futures-buffered", + "futures-lite", + "getrandom 0.4.1", + "log", + "lru", + "ntimestamp", + "reqwest 0.13.2", + "self_cell", + "serde", + "sha1_smol", + "simple-dns", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasm-bindgen-futures", +] + [[package]] name = "pkcs8" 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-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der 0.8.0", + "spki 0.8.0-rc.4", ] [[package]] @@ -1550,6 +3282,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "poly1305" version = "0.7.2" @@ -1596,6 +3341,42 @@ dependencies = [ "universal-hash 0.5.1", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portmapper" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d2a8825353ace3285138da3378b1e21860d60351942f7aa3b99b13b41f80318" +dependencies = [ + "base64", + "bytes", + "derive_more", + "futures-lite", + "futures-util", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "netwatch", + "num_enum", + "rand 0.9.2", + "serde", + "smallvec", + "socket2 0.6.2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + [[package]] name = "portpicker" version = "0.1.1" @@ -1605,6 +3386,40 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1647,6 +3462,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1656,6 +3481,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1665,15 +3499,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quicnprotochat-client" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "assert_cmd", "bincode", "capnp", "capnp-rpc", + "chacha20poly1305 0.10.1", "clap", "dashmap", "futures", @@ -1701,6 +3546,7 @@ dependencies = [ name = "quicnprotochat-core" version = "0.1.0" dependencies = [ + "argon2", "bincode", "capnp", "chacha20poly1305 0.10.1", @@ -1723,6 +3569,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "quicnprotochat-p2p" +version = "0.1.0" +dependencies = [ + "anyhow", + "iroh", + "tokio", + "tracing", +] + [[package]] name = "quicnprotochat-proto" version = "0.1.0" @@ -1753,6 +3609,7 @@ dependencies = [ "rustls", "serde", "sha2 0.10.9", + "subtle", "thiserror 1.0.69", "tokio", "tokio-util", @@ -1774,7 +3631,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.6.2", "thiserror 2.0.18", "tokio", "tracing", @@ -1787,6 +3644,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "fastbloom", "getrandom 0.3.4", @@ -1813,7 +3671,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.2", "tracing", "windows-sys 0.60.2", ] @@ -1992,6 +3850,88 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" @@ -2070,6 +4010,8 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -2133,6 +4075,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2144,6 +4087,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2162,6 +4111,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2175,9 +4130,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "zeroize", @@ -2206,12 +4161,34 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.228" @@ -2222,6 +4199,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.228" @@ -2264,6 +4251,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serdect" version = "0.2.0" @@ -2274,6 +4273,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.9.9" @@ -2298,6 +4303,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.10", +] + [[package]] name = "sha3" version = "0.10.8" @@ -2349,6 +4365,27 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" +dependencies = [ + "bitflags", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -2367,6 +4404,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -2377,6 +4424,38 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -2384,15 +4463,52 @@ 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-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der 0.8.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2416,6 +4532,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tempfile" version = "3.25.0" @@ -2491,10 +4633,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", + "js-sys", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", + "time-macros", ] [[package]] @@ -2503,6 +4650,26 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -2574,7 +4741,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -2590,6 +4757,28 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -2600,10 +4789,33 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.3.4", + "http", + "httparse", + "rand 0.9.2", + "ring", + "rustls-pki-types", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + [[package]] name = "toml" version = "0.8.23" @@ -2612,8 +4824,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -2625,6 +4837,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2634,17 +4855,83 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -2707,6 +4994,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -2719,6 +5012,18 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.4.0" @@ -2735,7 +5040,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -2745,12 +5050,42 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2763,6 +5098,54 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib 9.1.0", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib 0.1.6", +] + +[[package]] +name = "vergen-lib" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + [[package]] name = "version_check" version = "0.9.5" @@ -2807,6 +5190,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2828,6 +5220,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -2841,6 +5242,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -2873,6 +5288,63 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2892,6 +5364,37 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2901,12 +5404,113 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -2916,6 +5520,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2925,6 +5538,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2958,6 +5580,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2991,12 +5628,27 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3015,6 +5667,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3033,6 +5691,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3063,6 +5727,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3081,6 +5751,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3099,6 +5775,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3117,6 +5799,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3138,11 +5826,143 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wmi" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d9da833ef7c4419d8c3a18f0f7a8eca8ccc85f7ab8f359281c24100251211" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows", + "windows-core", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "x25519-dalek" @@ -3168,6 +5988,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yasna" version = "0.5.2" @@ -3177,6 +6012,35 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "z32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" + [[package]] name = "zerocopy" version = "0.8.39" @@ -3197,6 +6061,27 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" @@ -3218,6 +6103,39 @@ dependencies = [ "syn", ] +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index cf267b4..a1c32ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/quicnprotochat-proto", "crates/quicnprotochat-server", "crates/quicnprotochat-client", + "crates/quicnprotochat-p2p", ] # Shared dependency versions — bump here to affect the whole workspace. @@ -25,8 +26,10 @@ ed25519-dalek = { version = "2", features = ["rand_core"] } sha2 = { version = "0.10" } hkdf = { version = "0.12" } chacha20poly1305 = { version = "0.10" } -opaque-ke = { version = "4", features = ["ristretto255"] } -zeroize = { version = "1", features = ["derive"] } +opaque-ke = { version = "4", features = ["ristretto255", "argon2"] } +zeroize = { version = "1", features = ["derive", "serde"] } +subtle = { version = "2" } +argon2 = { version = "0.5" } rand = { version = "0.8" } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } @@ -42,7 +45,7 @@ tokio-util = { version = "0.7", features = ["codec", "compat"] } futures = { version = "0.3" } quinn = { version = "0.11" } quinn-proto = { version = "0.11" } -rustls = { version = "0.23", default-features = false, features = ["std"] } +rustls = { version = "0.23", default-features = false, features = ["std", "ring"] } rcgen = { version = "0.13" } # ── Database ───────────────────────────────────────────────────────────── diff --git a/crates/quicnprotochat-client/Cargo.toml b/crates/quicnprotochat-client/Cargo.toml index 5380d66..fbe054e 100644 --- a/crates/quicnprotochat-client/Cargo.toml +++ b/crates/quicnprotochat-client/Cargo.toml @@ -36,6 +36,8 @@ thiserror = { workspace = true } # Crypto — for fingerprint verification in fetch-key subcommand sha2 = { workspace = true } +argon2 = { workspace = true } +chacha20poly1305 = { workspace = true } quinn = { workspace = true } quinn-proto = { workspace = true } rustls = { workspace = true } diff --git a/crates/quicnprotochat-client/src/lib.rs b/crates/quicnprotochat-client/src/lib.rs index aaeaf15..0e00c25 100644 --- a/crates/quicnprotochat-client/src/lib.rs +++ b/crates/quicnprotochat-client/src/lib.rs @@ -4,7 +4,13 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use anyhow::Context; +use argon2::Argon2; use capnp_rpc::{rpc_twoparty_capnp::Side, twoparty, RpcSystem}; +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + ChaCha20Poly1305, Key, Nonce, +}; +use rand::RngCore; use serde::{Deserialize, Serialize}; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; @@ -13,12 +19,21 @@ use quinn_proto::crypto::rustls::QuicClientConfig; use rustls::pki_types::CertificateDer; use rustls::{ClientConfig as RustlsClientConfig, RootCertStore}; +use opaque_ke::{ + ClientLogin, ClientLoginFinishParameters, ClientRegistration, + ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse, +}; use quicnprotochat_core::{ - generate_key_package, hybrid_decrypt, hybrid_encrypt, DiskKeyStore, GroupMember, - HybridKeypair, HybridKeypairBytes, HybridPublicKey, IdentityKeypair, + generate_key_package, hybrid_decrypt, hybrid_encrypt, opaque_auth::OpaqueSuite, DiskKeyStore, + GroupMember, HybridKeypair, HybridKeypairBytes, HybridPublicKey, IdentityKeypair, }; use quicnprotochat_proto::node_capnp::{auth, node_service}; +/// Magic bytes for encrypted client state files. +const STATE_MAGIC: &[u8; 4] = b"QPCE"; +const STATE_SALT_LEN: usize = 16; +const STATE_NONCE_LEN: usize = 12; + // Global auth context initialized once per process. static AUTH_CONTEXT: OnceLock = OnceLock::new(); @@ -49,7 +64,7 @@ pub fn init_auth(ctx: ClientAuth) { let _ = AUTH_CONTEXT.set(ctx); } -// ── Subcommand implementations ─────────────────────────────────────────────── +// -- Subcommand implementations ----------------------------------------------- /// Connect to `server`, call health, and print RTT over QUIC/TLS. pub async fn cmd_ping(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> { @@ -72,6 +87,161 @@ pub async fn cmd_ping(server: &str, ca_cert: &Path, server_name: &str) -> anyhow Ok(()) } +/// Register a new user account via the OPAQUE protocol. +/// +/// The server never sees the password in plaintext. +pub async fn cmd_register_user( + server: &str, + ca_cert: &Path, + server_name: &str, + username: &str, + password: &str, +) -> anyhow::Result<()> { + let mut rng = rand::rngs::OsRng; + + let node_client = connect_node(server, ca_cert, server_name).await?; + + // OPAQUE registration step 1: client -> server. + let reg_start = + ClientRegistration::::start(&mut rng, password.as_bytes()) + .map_err(|e| anyhow::anyhow!("OPAQUE register start: {e}"))?; + + let mut req = node_client.opaque_register_start_request(); + { + let mut p = req.get(); + p.set_username(username); + p.set_request(®_start.message.serialize()); + } + let resp = req + .send() + .promise + .await + .context("opaque_register_start RPC failed")?; + let response_bytes = resp + .get() + .context("register_start: bad response")? + .get_response() + .context("register_start: missing response")? + .to_vec(); + + let reg_response = RegistrationResponse::::deserialize(&response_bytes) + .map_err(|e| anyhow::anyhow!("invalid registration response: {e}"))?; + + // OPAQUE registration step 2: client finishes -> server. + let reg_finish = reg_start + .state + .finish( + &mut rng, + password.as_bytes(), + reg_response, + ClientRegistrationFinishParameters::::default(), + ) + .map_err(|e| anyhow::anyhow!("OPAQUE register finish: {e}"))?; + + let mut req = node_client.opaque_register_finish_request(); + { + let mut p = req.get(); + p.set_username(username); + p.set_upload(®_finish.message.serialize()); + // Identity-token binding: pass empty bytes (no state file available). + p.set_identity_key(&[]); + } + let resp = req + .send() + .promise + .await + .context("opaque_register_finish RPC failed")?; + let success = resp + .get() + .context("register_finish: bad response")? + .get_success(); + + anyhow::ensure!(success, "server rejected registration"); + + println!("user '{username}' registered successfully (OPAQUE)"); + Ok(()) +} + +/// Log in via the OPAQUE protocol and receive a session token. +/// +/// Returns the session token as a hex string. Use it as `--access-token` for +/// subsequent commands. +pub async fn cmd_login( + server: &str, + ca_cert: &Path, + server_name: &str, + username: &str, + password: &str, +) -> anyhow::Result<()> { + let mut rng = rand::rngs::OsRng; + + let node_client = connect_node(server, ca_cert, server_name).await?; + + // OPAQUE login step 1: client -> server. + let login_start = + ClientLogin::::start(&mut rng, password.as_bytes()) + .map_err(|e| anyhow::anyhow!("OPAQUE login start: {e}"))?; + + let mut req = node_client.opaque_login_start_request(); + { + let mut p = req.get(); + p.set_username(username); + p.set_request(&login_start.message.serialize()); + } + let resp = req + .send() + .promise + .await + .context("opaque_login_start RPC failed")?; + let response_bytes = resp + .get() + .context("login_start: bad response")? + .get_response() + .context("login_start: missing response")? + .to_vec(); + + let credential_response = CredentialResponse::::deserialize(&response_bytes) + .map_err(|e| anyhow::anyhow!("invalid credential response: {e}"))?; + + // OPAQUE login step 2: client finishes -> server. + let login_finish = login_start + .state + .finish( + &mut rng, + password.as_bytes(), + credential_response, + ClientLoginFinishParameters::::default(), + ) + .map_err(|e| anyhow::anyhow!("OPAQUE login finish (bad password?): {e}"))?; + + let mut req = node_client.opaque_login_finish_request(); + { + let mut p = req.get(); + p.set_username(username); + p.set_finalization(&login_finish.message.serialize()); + // Identity-token binding: pass empty bytes (no state file available). + p.set_identity_key(&[]); + } + let resp = req + .send() + .promise + .await + .context("opaque_login_finish RPC failed")?; + let session_token = resp + .get() + .context("login_finish: bad response")? + .get_session_token() + .context("login_finish: missing session_token")? + .to_vec(); + + anyhow::ensure!(!session_token.is_empty(), "server returned empty session token"); + + println!("login successful for '{username}'"); + println!("session_token: {}", hex::encode(&session_token)); + println!("(use as --access-token for subsequent commands)"); + Ok(()) +} + /// Generate a KeyPackage for a fresh identity and upload it to the AS. /// /// Must run on a `LocalSet` because capnp-rpc is `!Send`. @@ -128,8 +298,9 @@ pub async fn cmd_register_state( server: &str, ca_cert: &Path, server_name: &str, + password: Option<&str>, ) -> anyhow::Result<()> { - let state = load_or_init_state(state_path)?; + let state = load_or_init_state(state_path, password)?; let (mut member, hybrid_kp) = state.into_parts(state_path)?; let tls_bytes = member @@ -181,7 +352,7 @@ pub async fn cmd_register_state( println!("fingerprint : {}", hex::encode(&fingerprint)); println!("KeyPackage uploaded successfully."); - save_state(state_path, &member, hybrid_kp.as_ref())?; + save_state(state_path, &member, hybrid_kp.as_ref(), password)?; Ok(()) } @@ -241,7 +412,7 @@ pub async fn cmd_fetch_key( Ok(()) } -/// Run a complete Alice↔Bob MLS round-trip using the unified server endpoint. +/// Run a complete Alice/Bob MLS round-trip using the unified server endpoint. /// /// All payloads are wrapped in post-quantum hybrid envelopes (X25519 + ML-KEM-768). pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> { @@ -321,12 +492,12 @@ pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> bob.join_group(&welcome_bytes) .context("Bob join_group failed")?; - // Alice → Bob (hybrid-wrapped) + // Alice -> Bob (hybrid-wrapped) let ct_ab = alice .send_message(b"hello bob") .context("Alice send_message failed")?; let wrapped_ab = - hybrid_encrypt(&bob_hybrid_pk, &ct_ab).context("hybrid encrypt Alice→Bob")?; + hybrid_encrypt(&bob_hybrid_pk, &ct_ab).context("hybrid encrypt Alice->Bob")?; enqueue(&alice_ds, &bob_id.public_key_bytes(), &wrapped_ab).await?; let bob_msgs = fetch_all(&bob_ds, &bob_id.public_key_bytes()).await?; @@ -338,11 +509,11 @@ pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> .receive_message(&inner_ab)? .context("Bob expected application message from Alice")?; println!( - "Alice → Bob plaintext: {}", + "Alice -> Bob plaintext: {}", String::from_utf8_lossy(&ab_plaintext) ); - // Bob → Alice (hybrid-wrapped) + // Bob -> Alice (hybrid-wrapped) let alice_hybrid_pk = fetch_hybrid_key(&bob_node, &alice_id.public_key_bytes()) .await? .context("Alice hybrid key not found")?; @@ -350,7 +521,7 @@ pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> .send_message(b"hello alice") .context("Bob send_message failed")?; let wrapped_ba = - hybrid_encrypt(&alice_hybrid_pk, &ct_ba).context("hybrid encrypt Bob→Alice")?; + hybrid_encrypt(&alice_hybrid_pk, &ct_ba).context("hybrid encrypt Bob->Alice")?; enqueue(&bob_ds, &alice_id.public_key_bytes(), &wrapped_ba).await?; let alice_msgs = fetch_all(&alice_ds, &alice_id.public_key_bytes()).await?; @@ -363,7 +534,7 @@ pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> .receive_message(&inner_ba)? .context("Alice expected application message from Bob")?; println!( - "Bob → Alice plaintext: {}", + "Bob -> Alice plaintext: {}", String::from_utf8_lossy(&ba_plaintext) ); @@ -377,8 +548,9 @@ pub async fn cmd_create_group( state_path: &Path, _server: &str, group_id: &str, + password: Option<&str>, ) -> anyhow::Result<()> { - let state = load_or_init_state(state_path)?; + let state = load_or_init_state(state_path, password)?; let (mut member, hybrid_kp) = state.into_parts(state_path)?; anyhow::ensure!( @@ -390,7 +562,7 @@ pub async fn cmd_create_group( .create_group(group_id.as_bytes()) .context("create_group failed")?; - save_state(state_path, &member, hybrid_kp.as_ref())?; + save_state(state_path, &member, hybrid_kp.as_ref(), password)?; println!("group created: {group_id}"); Ok(()) } @@ -405,8 +577,9 @@ pub async fn cmd_invite( ca_cert: &Path, server_name: &str, peer_key_hex: &str, + password: Option<&str>, ) -> anyhow::Result<()> { - let state = load_existing_state(state_path)?; + let state = load_existing_state(state_path, password)?; let (mut member, hybrid_kp) = state.into_parts(state_path)?; let peer_key = decode_identity_key(peer_key_hex)?; @@ -421,7 +594,30 @@ pub async fn cmd_invite( .group_ref() .context("no active group; run create-group first")?; - let (_, welcome) = member.add_member(&peer_kp).context("add_member failed")?; + // Collect existing member identity keys *before* adding the new member, + // so we know who to fan-out the commit to. + let existing_members: Vec> = member + .member_identities() + .into_iter() + .filter(|k| k.as_slice() != member.identity().public_key_bytes()) + .collect(); + + let (commit, welcome) = member.add_member(&peer_kp).context("add_member failed")?; + + // Fan out the Commit to all existing members (excluding self and the + // new joiner who receives the Welcome instead). Fix 14. + for mk in &existing_members { + if mk.as_slice() == peer_key.as_slice() { + continue; + } + let peer_hpk = fetch_hybrid_key(&node_client, mk).await?; + let commit_payload = if let Some(ref pk) = peer_hpk { + hybrid_encrypt(pk, &commit).context("hybrid encrypt commit")? + } else { + commit.clone() + }; + enqueue(&node_client, mk, &commit_payload).await?; + } // Wrap welcome in hybrid envelope if peer has a hybrid public key. let peer_hybrid_pk = fetch_hybrid_key(&node_client, &peer_key).await?; @@ -433,10 +629,11 @@ pub async fn cmd_invite( enqueue(&node_client, &peer_key, &payload).await?; - save_state(state_path, &member, hybrid_kp.as_ref())?; + save_state(state_path, &member, hybrid_kp.as_ref(), password)?; println!( - "invited peer (welcome queued{})", - if peer_hybrid_pk.is_some() { ", hybrid-encrypted" } else { "" } + "invited peer (welcome queued{}, commit sent to {} existing member(s))", + if peer_hybrid_pk.is_some() { ", hybrid-encrypted" } else { "" }, + existing_members.len(), ); Ok(()) } @@ -449,8 +646,9 @@ pub async fn cmd_join( server: &str, ca_cert: &Path, server_name: &str, + password: Option<&str>, ) -> anyhow::Result<()> { - let state = load_existing_state(state_path)?; + let state = load_existing_state(state_path, password)?; let (mut member, hybrid_kp) = state.into_parts(state_path)?; anyhow::ensure!( @@ -472,7 +670,7 @@ pub async fn cmd_join( .join_group(&welcome_bytes) .context("join_group failed")?; - save_state(state_path, &member, hybrid_kp.as_ref())?; + save_state(state_path, &member, hybrid_kp.as_ref(), password)?; println!("joined group successfully"); Ok(()) } @@ -488,8 +686,9 @@ pub async fn cmd_send( server_name: &str, peer_key_hex: &str, msg: &str, + password: Option<&str>, ) -> anyhow::Result<()> { - let state = load_existing_state(state_path)?; + let state = load_existing_state(state_path, password)?; let (mut member, hybrid_kp) = state.into_parts(state_path)?; let peer_key = decode_identity_key(peer_key_hex)?; @@ -509,7 +708,7 @@ pub async fn cmd_send( enqueue(&node_client, &peer_key, &payload).await?; - save_state(state_path, &member, hybrid_kp.as_ref())?; + save_state(state_path, &member, hybrid_kp.as_ref(), password)?; println!( "message sent{}", if peer_hybrid_pk.is_some() { " (hybrid-encrypted)" } else { "" } @@ -527,8 +726,9 @@ pub async fn cmd_recv( server_name: &str, wait_ms: u64, stream: bool, + password: Option<&str>, ) -> anyhow::Result<()> { - let state = load_existing_state(state_path)?; + let state = load_existing_state(state_path, password)?; let (mut member, hybrid_kp) = state.into_parts(state_path)?; let client = connect_node(server, ca_cert, server_name).await?; @@ -555,7 +755,7 @@ pub async fn cmd_recv( } } - save_state(state_path, &member, hybrid_kp.as_ref())?; + save_state(state_path, &member, hybrid_kp.as_ref(), password)?; if !stream { return Ok(()); @@ -563,7 +763,7 @@ pub async fn cmd_recv( } } -// ── Shared helpers ─────────────────────────────────────────────────────────── +// -- Shared helpers ----------------------------------------------------------- /// Establish a QUIC/TLS connection and return a `NodeService` client. /// @@ -583,9 +783,10 @@ pub async fn connect_node( .add(CertificateDer::from(cert_bytes)) .context("add root cert")?; - let tls = RustlsClientConfig::builder() + let mut tls = RustlsClientConfig::builder() .with_root_certificates(roots) .with_no_client_auth(); + tls.alpn_protocols = vec![b"capnp".to_vec()]; let crypto = QuicClientConfig::try_from(tls) .map_err(|e| anyhow::anyhow!("invalid client TLS config: {e}"))?; @@ -709,6 +910,7 @@ pub async fn fetch_all( p.set_recipient_key(recipient_key); p.set_channel_id(&[]); p.set_version(1); + p.set_limit(0); // fetch all (backward compat) let mut auth = p.reborrow().init_auth(); set_auth(&mut auth); } @@ -742,6 +944,7 @@ pub async fn fetch_wait( p.set_timeout_ms(timeout_ms); p.set_channel_id(&[]); p.set_version(1); + p.set_limit(0); // fetch all (backward compat) let mut auth = p.reborrow().init_auth(); set_auth(&mut auth); } @@ -777,6 +980,8 @@ pub async fn upload_hybrid_key( let mut p = req.get(); p.set_identity_key(identity_key); p.set_hybrid_public_key(&hybrid_pk.to_bytes()); + let mut auth = p.reborrow().init_auth(); + set_auth(&mut auth); } req.send() .promise @@ -793,7 +998,12 @@ pub async fn fetch_hybrid_key( identity_key: &[u8], ) -> anyhow::Result> { let mut req = client.fetch_hybrid_key_request(); - req.get().set_identity_key(identity_key); + { + let mut p = req.get(); + p.set_identity_key(identity_key); + let mut auth = p.reborrow().init_auth(); + set_auth(&mut auth); + } let resp = req .send() @@ -848,6 +1058,9 @@ struct StoredState { /// Post-quantum hybrid keypair (X25519 + ML-KEM-768). `None` for legacy state files. #[serde(default)] hybrid_key: Option, + /// Cached member public keys for group participants (Fix 14 prep). + #[serde(default)] + member_keys: Vec>, } impl StoredState { @@ -881,17 +1094,82 @@ impl StoredState { identity_seed: member.identity_seed(), group, hybrid_key: hybrid_kp.map(|kp| kp.to_bytes()), + member_keys: Vec::new(), }) } } -fn load_or_init_state(path: &Path) -> anyhow::Result { +// -- Encrypted state file helpers --------------------------------------------- + +/// Derive a 32-byte key from a password and salt using Argon2id. +fn derive_state_key(password: &str, salt: &[u8]) -> anyhow::Result<[u8; 32]> { + let mut key = [0u8; 32]; + Argon2::default() + .hash_password_into(password.as_bytes(), salt, &mut key) + .map_err(|e| anyhow::anyhow!("argon2 key derivation failed: {e}"))?; + Ok(key) +} + +/// Encrypt `plaintext` with the QPCE format: magic(4) | salt(16) | nonce(12) | ciphertext. +fn encrypt_state(password: &str, plaintext: &[u8]) -> anyhow::Result> { + let mut salt = [0u8; STATE_SALT_LEN]; + rand::rngs::OsRng.fill_bytes(&mut salt); + + let mut nonce_bytes = [0u8; STATE_NONCE_LEN]; + rand::rngs::OsRng.fill_bytes(&mut nonce_bytes); + + let key = derive_state_key(password, &salt)?; + let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|e| anyhow::anyhow!("state encryption failed: {e}"))?; + + let mut out = Vec::with_capacity(4 + STATE_SALT_LEN + STATE_NONCE_LEN + ciphertext.len()); + out.extend_from_slice(STATE_MAGIC); + out.extend_from_slice(&salt); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + Ok(out) +} + +/// Decrypt a QPCE-formatted state file. Caller must verify magic prefix beforehand. +fn decrypt_state(password: &str, data: &[u8]) -> anyhow::Result> { + let header_len = 4 + STATE_SALT_LEN + STATE_NONCE_LEN; + anyhow::ensure!( + data.len() > header_len, + "encrypted state file too short ({} bytes)", + data.len() + ); + + let salt = &data[4..4 + STATE_SALT_LEN]; + let nonce_bytes = &data[4 + STATE_SALT_LEN..header_len]; + let ciphertext = &data[header_len..]; + + let key = derive_state_key(password, salt)?; + let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); + let nonce = Nonce::from_slice(nonce_bytes); + + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|_| anyhow::anyhow!("state decryption failed (wrong password?)"))?; + + Ok(plaintext) +} + +/// Returns true if raw bytes begin with the QPCE magic header. +fn is_encrypted_state(bytes: &[u8]) -> bool { + bytes.len() >= 4 && &bytes[..4] == STATE_MAGIC +} + +fn load_or_init_state(path: &Path, password: Option<&str>) -> anyhow::Result { if path.exists() { - let mut state = load_existing_state(path)?; + let mut state = load_existing_state(path, password)?; // Upgrade legacy state files: generate hybrid keypair if missing. if state.hybrid_key.is_none() { state.hybrid_key = Some(HybridKeypair::generate().to_bytes()); - write_state(path, &state)?; + write_state(path, &state, password)?; } return Ok(state); } @@ -901,29 +1179,46 @@ fn load_or_init_state(path: &Path) -> anyhow::Result { let key_store = DiskKeyStore::persistent(keystore_path(path))?; let member = GroupMember::new_with_state(Arc::new(identity), key_store, None); let state = StoredState::from_parts(&member, Some(&hybrid_kp))?; - write_state(path, &state)?; + write_state(path, &state, password)?; Ok(state) } -fn load_existing_state(path: &Path) -> anyhow::Result { +fn load_existing_state(path: &Path, password: Option<&str>) -> anyhow::Result { let bytes = std::fs::read(path).with_context(|| format!("read state file {path:?}"))?; - bincode::deserialize(&bytes).context("decode state") + + if is_encrypted_state(&bytes) { + let pw = password.context( + "state file is encrypted (QPCE); a password is required to decrypt it", + )?; + let plaintext = decrypt_state(pw, &bytes)?; + bincode::deserialize(&plaintext).context("decode encrypted state") + } else { + bincode::deserialize(&bytes).context("decode state") + } } fn save_state( path: &Path, member: &GroupMember, hybrid_kp: Option<&HybridKeypair>, + password: Option<&str>, ) -> anyhow::Result<()> { let state = StoredState::from_parts(member, hybrid_kp)?; - write_state(path, &state) + write_state(path, &state, password) } -fn write_state(path: &Path, state: &StoredState) -> anyhow::Result<()> { +fn write_state(path: &Path, state: &StoredState, password: Option<&str>) -> anyhow::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).with_context(|| format!("create dir {parent:?}"))?; } - let bytes = bincode::serialize(state).context("encode state")?; + let plaintext = bincode::serialize(state).context("encode state")?; + + let bytes = if let Some(pw) = password { + encrypt_state(pw, &plaintext)? + } else { + plaintext + }; + std::fs::write(path, bytes).with_context(|| format!("write state {path:?}"))?; Ok(()) } @@ -950,7 +1245,7 @@ fn current_timestamp_ms() -> u64 { .as_millis() as u64 } -// ── Hex encoding helper ───────────────────────────────────────────────────── +// -- Hex encoding helper ------------------------------------------------------ // // We use a tiny inline module rather than adding `hex` as a dependency. diff --git a/crates/quicnprotochat-client/src/main.rs b/crates/quicnprotochat-client/src/main.rs index 4dc7515..fb7e946 100644 --- a/crates/quicnprotochat-client/src/main.rs +++ b/crates/quicnprotochat-client/src/main.rs @@ -5,8 +5,9 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; use quicnprotochat_client::{ - cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_invite, cmd_join, cmd_ping, cmd_recv, - cmd_register, cmd_register_state, cmd_send, ClientAuth, init_auth, + cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_invite, cmd_join, cmd_login, cmd_ping, + cmd_recv, cmd_register, cmd_register_state, cmd_register_user, cmd_send, ClientAuth, + init_auth, }; // ── CLI ─────────────────────────────────────────────────────────────────────── @@ -32,20 +33,48 @@ struct Args { )] server_name: String, - /// Bearer token for authenticated requests (version 1, required). - #[arg(long, global = true, env = "QUICNPROTOCHAT_ACCESS_TOKEN", required = true)] + /// Bearer token or OPAQUE session token for authenticated requests. + /// Not required for register-user and login commands. + #[arg(long, global = true, env = "QUICNPROTOCHAT_ACCESS_TOKEN", default_value = "")] access_token: String, /// Optional device identifier (UUID bytes encoded as hex or raw string). #[arg(long, global = true, env = "QUICNPROTOCHAT_DEVICE_ID")] device_id: Option, + /// Password to encrypt/decrypt client state files (QPCE format). + /// If set, state files are encrypted at rest with Argon2id + ChaCha20Poly1305. + #[arg(long, global = true, env = "QUICNPROTOCHAT_STATE_PASSWORD")] + state_password: Option, + #[command(subcommand)] command: Command, } #[derive(Debug, Subcommand)] enum Command { + /// Register a new user via OPAQUE (password never leaves the client). + RegisterUser { + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + server: String, + /// Username for the new account. + #[arg(long)] + username: String, + /// Password (will be used in OPAQUE PAKE; server never sees it). + #[arg(long)] + password: String, + }, + + /// Log in via OPAQUE and receive a session token. + Login { + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + server: String, + #[arg(long)] + username: String, + #[arg(long)] + password: String, + }, + /// Send a Ping to the server and print the round-trip time. Ping { /// Server address (host:port). @@ -54,9 +83,6 @@ enum Command { }, /// Generate a fresh MLS KeyPackage and upload it to the Authentication Service. - /// - /// Prints the SHA-256 fingerprint of the uploaded package and the raw - /// Ed25519 identity public key bytes (hex), which peers need to fetch it. Register { /// Server address (host:port). #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] @@ -64,9 +90,6 @@ enum Command { }, /// Fetch a peer's KeyPackage from the Authentication Service. - /// - /// IDENTITY_KEY is the peer's Ed25519 public key encoded as 64 lowercase - /// hex characters (32 bytes). FetchKey { /// Server address (host:port). #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] @@ -76,7 +99,7 @@ enum Command { identity_key: String, }, - /// Run a full Alice↔Bob MLS round-trip against live AS and DS endpoints. + /// Run a full Alice/Bob MLS round-trip against live AS and DS endpoints. DemoGroup { /// Server address (host:port). #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] @@ -140,7 +163,7 @@ enum Command { env = "QUICNPROTOCHAT_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:4201", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] server: String, }, @@ -152,7 +175,7 @@ enum Command { env = "QUICNPROTOCHAT_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:4201", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] server: String, /// Recipient identity key (hex, 32 bytes -> 64 chars). #[arg(long)] @@ -170,7 +193,7 @@ enum Command { env = "QUICNPROTOCHAT_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:4201", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] server: String, /// Wait for up to this many milliseconds if no messages are queued. @@ -196,11 +219,45 @@ async fn main() -> anyhow::Result<()> { let args = Args::parse(); - // Initialize auth context once for all RPCs. + // Initialize auth context once for all RPCs (empty token OK for register-user/login). let auth_ctx = ClientAuth::from_parts(args.access_token.clone(), args.device_id.clone()); init_auth(auth_ctx); + let state_pw = args.state_password.as_deref(); + match args.command { + Command::RegisterUser { + server, + username, + password, + } => { + let local = tokio::task::LocalSet::new(); + local + .run_until(cmd_register_user( + &server, + &args.ca_cert, + &args.server_name, + &username, + &password, + )) + .await + } + Command::Login { + server, + username, + password, + } => { + let local = tokio::task::LocalSet::new(); + local + .run_until(cmd_login( + &server, + &args.ca_cert, + &args.server_name, + &username, + &password, + )) + .await + } Command::Ping { server } => cmd_ping(&server, &args.ca_cert, &args.server_name).await, Command::Register { server } => { let local = tokio::task::LocalSet::new(); @@ -236,6 +293,7 @@ async fn main() -> anyhow::Result<()> { &server, &args.ca_cert, &args.server_name, + state_pw, )) .await } @@ -246,7 +304,7 @@ async fn main() -> anyhow::Result<()> { } => { let local = tokio::task::LocalSet::new(); local - .run_until(cmd_create_group(&state, &server, &group_id)) + .run_until(cmd_create_group(&state, &server, &group_id, state_pw)) .await } Command::Invite { @@ -262,13 +320,14 @@ async fn main() -> anyhow::Result<()> { &args.ca_cert, &args.server_name, &peer_key, + state_pw, )) .await } Command::Join { state, server } => { let local = tokio::task::LocalSet::new(); local - .run_until(cmd_join(&state, &server, &args.ca_cert, &args.server_name)) + .run_until(cmd_join(&state, &server, &args.ca_cert, &args.server_name, state_pw)) .await } Command::Send { @@ -286,6 +345,7 @@ async fn main() -> anyhow::Result<()> { &args.server_name, &peer_key, &msg, + state_pw, )) .await } @@ -304,8 +364,9 @@ async fn main() -> anyhow::Result<()> { &args.server_name, wait_ms, stream, + state_pw, )) .await } } -} \ No newline at end of file +} diff --git a/crates/quicnprotochat-client/tests/e2e.rs b/crates/quicnprotochat-client/tests/e2e.rs index 396a3ed..1f468e1 100644 --- a/crates/quicnprotochat-client/tests/e2e.rs +++ b/crates/quicnprotochat-client/tests/e2e.rs @@ -93,6 +93,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { &server, &ca_cert, "localhost", + None, )) .await?; @@ -102,6 +103,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { &server, &ca_cert, "localhost", + None, )) .await?; @@ -110,6 +112,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { &alice_state, &server, "test-group", + None, )) .await?; @@ -126,6 +129,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { &ca_cert, "localhost", &bob_pk_hex, + None, )) .await?; @@ -135,6 +139,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { &server, &ca_cert, "localhost", + None, )) .await?; @@ -147,6 +152,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { "localhost", &bob_pk_hex, "hello bob", + None, )) .await?; diff --git a/crates/quicnprotochat-core/Cargo.toml b/crates/quicnprotochat-core/Cargo.toml index d091bbe..a191279 100644 --- a/crates/quicnprotochat-core/Cargo.toml +++ b/crates/quicnprotochat-core/Cargo.toml @@ -20,6 +20,7 @@ ml-kem = { workspace = true } # Crypto — OPAQUE password-authenticated key exchange opaque-ke = { workspace = true } +argon2 = { workspace = true } # Crypto — MLS (M2) openmls = { workspace = true } diff --git a/crates/quicnprotochat-core/src/group.rs b/crates/quicnprotochat-core/src/group.rs index 2671a5a..4a19d4b 100644 --- a/crates/quicnprotochat-core/src/group.rs +++ b/crates/quicnprotochat-core/src/group.rs @@ -361,6 +361,21 @@ impl GroupMember { self.group.as_ref() } + /// Return the identity (credential) bytes of all current group members. + /// + /// Each entry is the raw credential payload (Ed25519 public key bytes) + /// extracted from the member's MLS leaf node. + pub fn member_identities(&self) -> Vec> { + let group = match self.group.as_ref() { + Some(g) => g, + None => return Vec::new(), + }; + group + .members() + .map(|m| m.credential.identity().to_vec()) + .collect() + } + // ── Private helpers ─────────────────────────────────────────────────────── fn make_credential_with_key(&self) -> Result { diff --git a/crates/quicnprotochat-core/src/hybrid_kem.rs b/crates/quicnprotochat-core/src/hybrid_kem.rs index df02840..62c9480 100644 --- a/crates/quicnprotochat-core/src/hybrid_kem.rs +++ b/crates/quicnprotochat-core/src/hybrid_kem.rs @@ -28,7 +28,7 @@ use ml_kem::{ kem::{Decapsulate, Encapsulate}, EncodedSizeUser, KemCore, MlKem768, MlKem768Params, }; -use rand::rngs::OsRng; +use rand::{rngs::OsRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use x25519_dalek::{EphemeralSecret, PublicKey as X25519Public, StaticSecret}; @@ -92,10 +92,13 @@ pub struct HybridKeypair { } /// Serialisable form of a [`HybridKeypair`] for persistence. +/// +/// Secret fields are wrapped in [`Zeroizing`] so they are securely erased +/// when the struct is dropped. #[derive(Serialize, Deserialize)] pub struct HybridKeypairBytes { - pub x25519_sk: [u8; 32], - pub mlkem_dk: Vec, + pub x25519_sk: Zeroizing<[u8; 32]>, + pub mlkem_dk: Zeroizing>, pub mlkem_ek: Vec, } @@ -123,7 +126,7 @@ impl HybridKeypair { /// Reconstruct from serialised bytes. pub fn from_bytes(bytes: &HybridKeypairBytes) -> Result { - let x25519_sk = StaticSecret::from(bytes.x25519_sk); + let x25519_sk = StaticSecret::from(*bytes.x25519_sk); let x25519_pk = X25519Public::from(&x25519_sk); let mlkem_dk_arr = Array::try_from(bytes.mlkem_dk.as_slice()) @@ -145,8 +148,8 @@ impl HybridKeypair { /// Serialise the keypair for persistence. pub fn to_bytes(&self) -> HybridKeypairBytes { HybridKeypairBytes { - x25519_sk: self.x25519_sk.to_bytes(), - mlkem_dk: self.mlkem_dk.as_bytes().to_vec(), + x25519_sk: Zeroizing::new(self.x25519_sk.to_bytes()), + mlkem_dk: Zeroizing::new(self.mlkem_dk.as_bytes().to_vec()), mlkem_ek: self.mlkem_ek.as_bytes().to_vec(), } } @@ -207,9 +210,13 @@ pub fn hybrid_encrypt( .encapsulate(&mut OsRng) .map_err(|_| HybridKemError::EncryptionFailed)?; - // 3. Combine shared secrets via HKDF - let (aead_key, aead_nonce) = - derive_aead_material(x25519_ss.as_bytes(), mlkem_ss.as_slice()); + // 3. Derive AEAD key from combined shared secrets + let aead_key = derive_aead_key(x25519_ss.as_bytes(), mlkem_ss.as_slice()); + + // Generate a random 12-byte nonce (not derived from HKDF). + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let aead_nonce = *Nonce::from_slice(&nonce_bytes); // 4. AEAD encrypt let cipher = ChaCha20Poly1305::new(&aead_key); @@ -275,7 +282,7 @@ pub fn hybrid_decrypt( .map_err(|_| HybridKemError::MlKemDecapsFailed)?; // 3. Derive AEAD key - let (aead_key, _) = derive_aead_material(x25519_ss.as_bytes(), mlkem_ss.as_slice()); + let aead_key = derive_aead_key(x25519_ss.as_bytes(), mlkem_ss.as_slice()); // 4. Decrypt let cipher = ChaCha20Poly1305::new(&aead_key); @@ -286,11 +293,12 @@ pub fn hybrid_decrypt( Ok(plaintext) } -/// Derive AEAD key + nonce from the combined X25519 + ML-KEM shared secrets. -fn derive_aead_material( - x25519_ss: &[u8], - mlkem_ss: &[u8], -) -> (Key, Nonce) { +/// Derive AEAD key from the combined X25519 + ML-KEM shared secrets. +/// +/// The nonce is generated randomly per-encryption rather than derived from +/// HKDF, preventing nonce reuse when the same shared secret is (accidentally) +/// used more than once. +fn derive_aead_key(x25519_ss: &[u8], mlkem_ss: &[u8]) -> Key { let mut ikm = Zeroizing::new(vec![0u8; x25519_ss.len() + mlkem_ss.len()]); ikm[..x25519_ss.len()].copy_from_slice(x25519_ss); ikm[x25519_ss.len()..].copy_from_slice(mlkem_ss); @@ -301,11 +309,7 @@ fn derive_aead_material( hk.expand(HKDF_INFO, &mut *key_bytes) .expect("32 bytes is valid HKDF-SHA256 output length"); - let mut nonce_bytes = [0u8; 12]; - hk.expand(b"quicnprotochat-hybrid-nonce-v1", &mut nonce_bytes) - .expect("12 bytes is valid HKDF-SHA256 output length"); - - (*Key::from_slice(&*key_bytes), *Nonce::from_slice(&nonce_bytes)) + *Key::from_slice(&*key_bytes) } // ── Tests ─────────────────────────────────────────────────────────────────── diff --git a/crates/quicnprotochat-core/src/opaque_auth.rs b/crates/quicnprotochat-core/src/opaque_auth.rs index 125e57e..c75519c 100644 --- a/crates/quicnprotochat-core/src/opaque_auth.rs +++ b/crates/quicnprotochat-core/src/opaque_auth.rs @@ -9,7 +9,7 @@ use opaque_ke::CipherSuite; /// /// - **OPRF**: Ristretto255 (curve25519-based, ~128-bit security) /// - **Key exchange**: Triple-DH (3DH) over Ristretto255 with SHA-512 -/// - **KSF**: Identity (no key stretching; upgrade to Argon2 later) +/// - **KSF**: Argon2id (memory-hard key stretching) pub struct OpaqueSuite; impl CipherSuite for OpaqueSuite { @@ -18,5 +18,5 @@ impl CipherSuite for OpaqueSuite { opaque_ke::Ristretto255, sha2::Sha512, >; - type Ksf = opaque_ke::ksf::Identity; + type Ksf = argon2::Argon2<'static>; } diff --git a/crates/quicnprotochat-proto/build.rs b/crates/quicnprotochat-proto/build.rs index 54c3d3d..792c8d1 100644 --- a/crates/quicnprotochat-proto/build.rs +++ b/crates/quicnprotochat-proto/build.rs @@ -26,10 +26,6 @@ fn main() { let schemas_dir = workspace_root.join("schemas"); // Re-run this build script whenever any schema file changes. - println!( - "cargo:rerun-if-changed={}", - schemas_dir.join("envelope.capnp").display() - ); println!( "cargo:rerun-if-changed={}", schemas_dir.join("auth.capnp").display() @@ -47,7 +43,6 @@ fn main() { // Treat `schemas/` as the include root so that inter-schema imports // resolve correctly. .src_prefix(&schemas_dir) - .file(schemas_dir.join("envelope.capnp")) .file(schemas_dir.join("auth.capnp")) .file(schemas_dir.join("delivery.capnp")) .file(schemas_dir.join("node.capnp")) diff --git a/crates/quicnprotochat-proto/src/lib.rs b/crates/quicnprotochat-proto/src/lib.rs index 30ea88a..636cc71 100644 --- a/crates/quicnprotochat-proto/src/lib.rs +++ b/crates/quicnprotochat-proto/src/lib.rs @@ -11,22 +11,9 @@ //! //! `build.rs` invokes `capnpc` at compile time and writes generated Rust source //! into `$OUT_DIR`. The `include!` macros below splice that code in as a module. -//! -//! # Canonical serialisation (M2+) -//! -//! `build_envelope` uses standard Cap'n Proto wire format. Canonical serialisation -//! (deterministic byte representation for cryptographic signing of KeyPackages and -//! Commits) is added in M2 once the Authentication Service is introduced. // ── Generated types ─────────────────────────────────────────────────────────── -/// Cap'n Proto generated types for `schemas/envelope.capnp`. -/// -/// Do not edit this module by hand — it is entirely machine-generated. -pub mod envelope_capnp { - include!(concat!(env!("OUT_DIR"), "/envelope_capnp.rs")); -} - /// Cap'n Proto generated types for `schemas/auth.capnp`. /// /// Do not edit this module by hand — it is entirely machine-generated. @@ -48,95 +35,6 @@ pub mod node_capnp { include!(concat!(env!("OUT_DIR"), "/node_capnp.rs")); } -// ── Re-exports ──────────────────────────────────────────────────────────────── - -/// The message-type discriminant from the `Envelope` schema. -/// -/// Re-exported here so callers can `use quicnprotochat_proto::MsgType` without -/// spelling out the full generated module path. -pub use envelope_capnp::envelope::MsgType; - -// ── Owned envelope type ─────────────────────────────────────────────────────── - -/// An owned, decoded `Envelope` with no Cap'n Proto reader lifetimes. -/// -/// All byte fields are eagerly copied out of the Cap'n Proto reader so that -/// this type is `Send + 'static` and can cross async task boundaries freely. -/// -/// # Invariants -/// -/// - `group_id` and `sender_id` are either empty (for control messages such as -/// `Ping`/`Pong`) or exactly 32 bytes (SHA-256 digest). -/// - `payload` is empty for `Ping` and `Pong`; non-empty for all MLS variants. -#[derive(Debug, Clone)] -pub struct ParsedEnvelope { - pub msg_type: MsgType, - /// SHA-256 of the group name, or empty for point-to-point control messages. - pub group_id: Vec, - /// SHA-256 of the sender's Ed25519 identity public key, or empty. - pub sender_id: Vec, - /// Opaque payload — interpretation is determined by `msg_type`. - pub payload: Vec, - /// Unix timestamp in milliseconds. - pub timestamp_ms: u64, -} - -// ── Serialisation helpers ───────────────────────────────────────────────────── - -/// Serialise a [`ParsedEnvelope`] to unpacked Cap'n Proto wire bytes. -/// -/// The returned bytes include the Cap'n Proto segment table header followed by -/// the message data. They are suitable for use as the body of a length-prefixed -/// quicnprotochat frame (the frame codec in `quicnprotochat-core` prepends the 4-byte length). -/// -/// # Errors -/// -/// Returns [`capnp::Error`] if the underlying allocator fails (out of memory). -/// This is not expected under normal operation. -pub fn build_envelope(env: &ParsedEnvelope) -> Result, capnp::Error> { - use capnp::message; - - let mut message = message::Builder::new_default(); - { - let mut root = message.init_root::(); - root.set_msg_type(env.msg_type); - root.set_group_id(&env.group_id); - root.set_sender_id(&env.sender_id); - root.set_payload(&env.payload); - root.set_timestamp_ms(env.timestamp_ms); - } - to_bytes(&message) -} - -/// Deserialise unpacked Cap'n Proto wire bytes into a [`ParsedEnvelope`]. -/// -/// All data is copied out of the Cap'n Proto reader before returning, so the -/// input slice is not retained. -/// -/// # Errors -/// -/// - [`capnp::Error`] if the bytes are not valid Cap'n Proto wire format. -/// - [`capnp::Error`] if `msgType` contains a discriminant not present in the -/// current schema (forward-compatibility guard). -pub fn parse_envelope(bytes: &[u8]) -> Result { - let reader = from_bytes(bytes)?; - let root = reader.get_root::()?; - - let msg_type = root.get_msg_type().map_err(|nis| { - capnp::Error::failed(format!( - "Envelope.msgType contains unknown discriminant: {nis}" - )) - })?; - - Ok(ParsedEnvelope { - msg_type, - group_id: root.get_group_id()?.to_vec(), - sender_id: root.get_sender_id()?.to_vec(), - payload: root.get_payload()?.to_vec(), - timestamp_ms: root.get_timestamp_ms(), - }) -} - // ── Low-level byte ↔ message conversions ────────────────────────────────────── /// Serialise a Cap'n Proto message builder to unpacked wire bytes. @@ -162,57 +60,3 @@ pub fn from_bytes( let mut cursor = std::io::Cursor::new(bytes); capnp::serialize::read_message(&mut cursor, capnp::message::ReaderOptions::new()) } - -// ── Tests ───────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - - /// Round-trip a Ping envelope through build → parse and verify all fields. - #[test] - fn ping_round_trip() { - let original = ParsedEnvelope { - msg_type: MsgType::Ping, - group_id: vec![], - sender_id: vec![0xAB; 32], - payload: vec![], - timestamp_ms: 1_700_000_000_000, - }; - - let bytes = build_envelope(&original).expect("build_envelope failed"); - let parsed = parse_envelope(&bytes).expect("parse_envelope failed"); - - assert!(matches!(parsed.msg_type, MsgType::Ping)); - assert_eq!(parsed.group_id, original.group_id); - assert_eq!(parsed.sender_id, original.sender_id); - assert_eq!(parsed.payload, original.payload); - assert_eq!(parsed.timestamp_ms, original.timestamp_ms); - } - - /// Round-trip a Pong envelope. - #[test] - fn pong_round_trip() { - let original = ParsedEnvelope { - msg_type: MsgType::Pong, - group_id: vec![], - sender_id: vec![0xCD; 32], - payload: vec![], - timestamp_ms: 1_700_000_001_000, - }; - - let bytes = build_envelope(&original).expect("build_envelope failed"); - let parsed = parse_envelope(&bytes).expect("parse_envelope failed"); - - assert!(matches!(parsed.msg_type, MsgType::Pong)); - assert_eq!(parsed.sender_id, original.sender_id); - assert_eq!(parsed.timestamp_ms, original.timestamp_ms); - } - - /// Corrupted bytes must produce an error, not a panic. - #[test] - fn corrupted_bytes_error() { - let result = parse_envelope(&[0xFF, 0xFF, 0xFF, 0xFF]); - assert!(result.is_err(), "expected error for corrupted input"); - } -} diff --git a/crates/quicnprotochat-server/Cargo.toml b/crates/quicnprotochat-server/Cargo.toml index 18294db..71652d7 100644 --- a/crates/quicnprotochat-server/Cargo.toml +++ b/crates/quicnprotochat-server/Cargo.toml @@ -35,6 +35,7 @@ rcgen = { workspace = true } # Crypto — OPAQUE PAKE opaque-ke = { workspace = true } rand = { workspace = true } +subtle = { workspace = true } # Database rusqlite = { workspace = true } diff --git a/crates/quicnprotochat-server/src/error_codes.rs b/crates/quicnprotochat-server/src/error_codes.rs new file mode 100644 index 0000000..60d90ea --- /dev/null +++ b/crates/quicnprotochat-server/src/error_codes.rs @@ -0,0 +1,30 @@ +//! Structured error codes for server RPC responses. +//! +//! Every `capnp::Error::failed()` message is prefixed with a stable code +//! (E001–E020) so clients can match on the code without parsing free-text. + +pub const E001_BAD_AUTH_VERSION: &str = "E001"; +pub const E002_EMPTY_TOKEN: &str = "E002"; +pub const E003_INVALID_TOKEN: &str = "E003"; +pub const E004_IDENTITY_KEY_LENGTH: &str = "E004"; +pub const E005_PAYLOAD_EMPTY: &str = "E005"; +pub const E006_PAYLOAD_TOO_LARGE: &str = "E006"; +pub const E007_PACKAGE_EMPTY: &str = "E007"; +pub const E008_PACKAGE_TOO_LARGE: &str = "E008"; +pub const E009_STORAGE_ERROR: &str = "E009"; +pub const E010_OPAQUE_ERROR: &str = "E010"; +pub const E011_USERNAME_EMPTY: &str = "E011"; +pub const E012_WIRE_VERSION: &str = "E012"; +pub const E013_HYBRID_KEY_EMPTY: &str = "E013"; +pub const E014_RATE_LIMITED: &str = "E014"; +pub const E015_QUEUE_FULL: &str = "E015"; +pub const E016_IDENTITY_MISMATCH: &str = "E016"; +pub const E017_SESSION_EXPIRED: &str = "E017"; +pub const E018_USER_EXISTS: &str = "E018"; +pub const E019_NO_PENDING_LOGIN: &str = "E019"; +pub const E020_BAD_PARAMS: &str = "E020"; + +/// Build a `capnp::Error::failed()` with the structured code prefix. +pub fn coded_error(code: &str, msg: impl std::fmt::Display) -> capnp::Error { + capnp::Error::failed(format!("{code}: {msg}")) +} diff --git a/crates/quicnprotochat-server/src/main.rs b/crates/quicnprotochat-server/src/main.rs index 681eb52..06d4bcf 100644 --- a/crates/quicnprotochat-server/src/main.rs +++ b/crates/quicnprotochat-server/src/main.rs @@ -1,14 +1,9 @@ //! quicnprotochat-server — unified Authentication + Delivery service. //! -//! # M3 scope -//! -//! The server exposes a single QUIC + TLS 1.3 Cap'n Proto RPC endpoint -//! (`NodeService`) combining Authentication and Delivery operations. -//! //! # Architecture //! //! ```text -//! QUIC endpoint (4201) +//! QUIC endpoint (7000) //! └─ TLS 1.3 handshake (self-signed by default) //! └─ capnp-rpc VatNetwork (LocalSet, !Send) //! └─ NodeServiceImpl (KeyPackage + Delivery queues) @@ -17,13 +12,6 @@ //! Because `capnp-rpc` uses `Rc>` internally it is `!Send`. //! The entire RPC stack lives on a `tokio::task::LocalSet` spawned per //! connection. -//! -//! # Configuration -//! -//! | Env var | CLI flag | Default | -//! |---------------------|----------------|-----------------| -//! | `QUICNPROTOCHAT_LISTEN` | `--listen` | `0.0.0.0:4201` | -//! | `RUST_LOG` | — | `info` | use std::{fs, net::SocketAddr, path::{Path, PathBuf}, sync::Arc, time::Duration}; @@ -33,19 +21,28 @@ use capnp::capability::Promise; use capnp_rpc::{rpc_twoparty_capnp::Side, twoparty, RpcSystem}; use clap::Parser; use dashmap::DashMap; +use opaque_ke::{ + CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload, + ServerLogin, ServerRegistration, ServerSetup, +}; +use quicnprotochat_core::opaque_auth::OpaqueSuite; use quicnprotochat_proto::node_capnp::{auth, node_service}; use quinn::{Endpoint, ServerConfig}; use quinn_proto::crypto::rustls::QuicServerConfig; +use rand::rngs::OsRng; use rcgen::generate_simple_self_signed; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::version::TLS13; use sha2::{Digest, Sha256}; +use subtle::ConstantTimeEq; use tokio::sync::Notify; use tokio::time::timeout; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; +mod error_codes; mod sql_store; mod storage; +use error_codes::*; use sql_store::SqlStore; use storage::{FileBackedStore, Store, StorageError}; @@ -60,6 +57,13 @@ const DEFAULT_TLS_KEY: &str = "data/server-key.der"; const DEFAULT_STORE_BACKEND: &str = "file"; const DEFAULT_DB_PATH: &str = "data/quicnprotochat.db"; +const SESSION_TTL_SECS: u64 = 24 * 60 * 60; // 24 hours +const PENDING_LOGIN_TTL_SECS: u64 = 300; // 5 minutes +const RATE_LIMIT_WINDOW_SECS: u64 = 60; +const RATE_LIMIT_MAX_ENQUEUES: u32 = 100; +const MAX_QUEUE_DEPTH: usize = 1000; +const MESSAGE_TTL_SECS: u64 = 7 * 24 * 60 * 60; // 7 days + #[derive(Clone, Debug)] struct AuthConfig { required_token: Option>, @@ -224,6 +228,35 @@ struct Args { db_key: String, } +// ── Session management ────────────────────────────────────────────────────── + +struct SessionInfo { + username: String, + identity_key: Vec, + #[allow(dead_code)] + created_at: u64, + expires_at: u64, +} + +/// Pending OPAQUE login state with expiry tracking. +struct PendingLogin { + state_bytes: Vec, + created_at: u64, +} + +/// Rate limiter entry for enqueue throttling. +struct RateEntry { + count: u32, + window_start: u64, +} + +fn current_timestamp() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + // ── Node service implementation ───────────────────────────────────────────── /// Cap'n Proto RPC server implementation for `NodeService` (Auth + Delivery). @@ -231,6 +264,13 @@ struct NodeServiceImpl { store: Arc, waiters: Arc, Arc>>, auth_cfg: Arc, + opaque_setup: Arc>, + /// Pending OPAQUE login states keyed by username. + pending_logins: Arc>, + /// Active session tokens → session info. + sessions: Arc, SessionInfo>>, + /// Per-token enqueue rate limiter. + rate_limits: Arc, RateEntry>>, } impl NodeServiceImpl { @@ -251,20 +291,20 @@ impl node_service::Server for NodeServiceImpl { ) -> Promise<(), capnp::Error> { let params = params .get() - .map_err(|e| capnp::Error::failed(format!("upload_key_package: bad params: {e}"))); + .map_err(|e| coded_error(E020_BAD_PARAMS, format!("upload_key_package: bad params: {e}"))); let (identity_key, package) = match params { Ok(p) => { - if let Err(e) = validate_auth(&self.auth_cfg, p.get_auth()) { + if let Err(e) = validate_auth(&self.auth_cfg, &self.sessions, p.get_auth()) { return Promise::err(e); } let ik = match p.get_identity_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let pkg = match p.get_package() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; (ik, pkg) } @@ -272,21 +312,19 @@ impl node_service::Server for NodeServiceImpl { }; if identity_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "identityKey must be exactly 32 bytes, got {}", - identity_key.len() - ))); - } - if package.is_empty() { - return Promise::err(capnp::Error::failed( - "package must not be empty".to_string(), + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), )); } + if package.is_empty() { + return Promise::err(coded_error(E007_PACKAGE_EMPTY, "package must not be empty")); + } if package.len() > MAX_KEYPACKAGE_BYTES { - return Promise::err(capnp::Error::failed(format!( - "package exceeds max size ({} bytes)", - MAX_KEYPACKAGE_BYTES - ))); + return Promise::err(coded_error( + E008_PACKAGE_TOO_LARGE, + format!("package exceeds max size ({} bytes)", MAX_KEYPACKAGE_BYTES), + )); } let fingerprint: Vec = Sha256::digest(&package).to_vec(); @@ -317,24 +355,24 @@ impl node_service::Server for NodeServiceImpl { let identity_key = match params.get() { Ok(p) => match p.get_identity_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }, - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; if let Err(e) = params .get() .ok() - .map(|p| validate_auth(&self.auth_cfg, p.get_auth())) + .map(|p| validate_auth(&self.auth_cfg, &self.sessions, p.get_auth())) .transpose() { return Promise::err(e); } if identity_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "identityKey must be exactly 32 bytes, got {}", - identity_key.len() - ))); + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), + )); } let package = match self @@ -374,44 +412,60 @@ impl node_service::Server for NodeServiceImpl { ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let recipient_key = match p.get_recipient_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let payload = match p.get_payload() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let channel_id = p.get_channel_id().unwrap_or_default().to_vec(); let version = p.get_version(); - if let Err(e) = validate_auth(&self.auth_cfg, p.get_auth()) { + let auth_token = match validate_auth_return_token(&self.auth_cfg, &self.sessions, p.get_auth()) { + Ok(t) => t, + Err(e) => return Promise::err(e), + }; + + if recipient_key.len() != 32 { + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("recipientKey must be exactly 32 bytes, got {}", recipient_key.len()), + )); + } + if payload.is_empty() { + return Promise::err(coded_error(E005_PAYLOAD_EMPTY, "payload must not be empty")); + } + if payload.len() > MAX_PAYLOAD_BYTES { + return Promise::err(coded_error( + E006_PAYLOAD_TOO_LARGE, + format!("payload exceeds max size ({} bytes)", MAX_PAYLOAD_BYTES), + )); + } + if version != CURRENT_WIRE_VERSION { + return Promise::err(coded_error( + E012_WIRE_VERSION, + format!("unsupported wire version {} (expected {CURRENT_WIRE_VERSION})", version), + )); + } + + // Rate limiting (Fix 6) + if let Err(e) = check_rate_limit(&self.rate_limits, &auth_token) { return Promise::err(e); } - if recipient_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "recipientKey must be exactly 32 bytes, got {}", - recipient_key.len() - ))); - } - if payload.is_empty() { - return Promise::err(capnp::Error::failed( - "payload must not be empty".to_string(), - )); - } - if payload.len() > MAX_PAYLOAD_BYTES { - return Promise::err(capnp::Error::failed(format!( - "payload exceeds max size ({} bytes)", - MAX_PAYLOAD_BYTES - ))); - } - if version != CURRENT_WIRE_VERSION { - return Promise::err(capnp::Error::failed(format!( - "unsupported wire version {} (expected {CURRENT_WIRE_VERSION})", - version - ))); + // Queue depth check (Fix 7) + match self.store.queue_depth(&recipient_key, &channel_id) { + Ok(depth) if depth >= MAX_QUEUE_DEPTH => { + return Promise::err(coded_error( + E015_QUEUE_FULL, + format!("queue depth {} exceeds limit {}", depth, MAX_QUEUE_DEPTH), + )); + } + Err(e) => return Promise::err(storage_err(e)), + _ => {} } if let Err(e) = self @@ -432,7 +486,7 @@ impl node_service::Server for NodeServiceImpl { Promise::ok(()) } - /// Atomically drain and return all queued payloads for `recipient_key`. + /// Atomically drain and return queued payloads for `recipient_key`. fn fetch( &mut self, params: node_service::FetchParams, @@ -441,9 +495,9 @@ impl node_service::Server for NodeServiceImpl { let recipient_key = match params.get() { Ok(p) => match p.get_recipient_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }, - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let channel_id = params .get() @@ -456,35 +510,43 @@ impl node_service::Server for NodeServiceImpl { .ok() .map(|p| p.get_version()) .unwrap_or(CURRENT_WIRE_VERSION); + let limit = params + .get() + .ok() + .map(|p| p.get_limit()) + .unwrap_or(0); if let Err(e) = params .get() .ok() - .map(|p| validate_auth(&self.auth_cfg, p.get_auth())) + .map(|p| validate_auth(&self.auth_cfg, &self.sessions, p.get_auth())) .transpose() { return Promise::err(e); } if recipient_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "recipientKey must be exactly 32 bytes, got {}", - recipient_key.len() - ))); + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("recipientKey must be exactly 32 bytes, got {}", recipient_key.len()), + )); } if version != CURRENT_WIRE_VERSION { - return Promise::err(capnp::Error::failed(format!( - "unsupported wire version {} (expected {CURRENT_WIRE_VERSION})", - version - ))); + return Promise::err(coded_error( + E012_WIRE_VERSION, + format!("unsupported wire version {} (expected {CURRENT_WIRE_VERSION})", version), + )); } - let messages = match self - .store - .fetch(&recipient_key, &channel_id) - .map_err(storage_err) - { - Ok(m) => m, - Err(e) => return Promise::err(e), + let messages = if limit > 0 { + match self.store.fetch_limited(&recipient_key, &channel_id, limit as usize).map_err(storage_err) { + Ok(m) => m, + Err(e) => return Promise::err(e), + } + } else { + match self.store.fetch(&recipient_key, &channel_id).map_err(storage_err) { + Ok(m) => m, + Err(e) => return Promise::err(e), + } }; tracing::debug!( @@ -509,39 +571,46 @@ impl node_service::Server for NodeServiceImpl { ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let recipient_key = match p.get_recipient_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let channel_id = p.get_channel_id().unwrap_or_default().to_vec(); let version = p.get_version(); let timeout_ms = p.get_timeout_ms(); - if let Err(e) = validate_auth(&self.auth_cfg, p.get_auth()) { + let limit = p.get_limit(); + if let Err(e) = validate_auth(&self.auth_cfg, &self.sessions, p.get_auth()) { return Promise::err(e); } if recipient_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "recipientKey must be exactly 32 bytes, got {}", - recipient_key.len() - ))); + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("recipientKey must be exactly 32 bytes, got {}", recipient_key.len()), + )); } if version != CURRENT_WIRE_VERSION { - return Promise::err(capnp::Error::failed(format!( - "unsupported wire version {} (expected {CURRENT_WIRE_VERSION})", - version - ))); + return Promise::err(coded_error( + E012_WIRE_VERSION, + format!("unsupported wire version {} (expected {CURRENT_WIRE_VERSION})", version), + )); } let store = Arc::clone(&self.store); let waiters = self.waiters.clone(); Promise::from_future(async move { - let messages = store - .fetch(&recipient_key, &channel_id) - .map_err(storage_err)?; + let fetch_fn = |s: &Arc, rk: &[u8], ch: &[u8], lim: u32| -> Result>, capnp::Error> { + if lim > 0 { + s.fetch_limited(rk, ch, lim as usize).map_err(storage_err) + } else { + s.fetch(rk, ch).map_err(storage_err) + } + }; + + let messages = fetch_fn(&store, &recipient_key, &channel_id, limit)?; if messages.is_empty() && timeout_ms > 0 { let waiter = waiters @@ -549,9 +618,7 @@ impl node_service::Server for NodeServiceImpl { .or_insert_with(|| Arc::new(Notify::new())) .clone(); let _ = timeout(Duration::from_millis(timeout_ms), waiter.notified()).await; - let msgs = store - .fetch(&recipient_key, &channel_id) - .map_err(storage_err)?; + let msgs = fetch_fn(&store, &recipient_key, &channel_id, limit)?; fill_payloads_wait(&mut results, msgs); return Ok(()); } @@ -578,26 +645,32 @@ impl node_service::Server for NodeServiceImpl { ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let identity_key = match p.get_identity_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let hybrid_pk = match p.get_hybrid_public_key() { Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; + // Fix 1: Auth required on hybrid key ops + if let Err(e) = validate_auth(&self.auth_cfg, &self.sessions, p.get_auth()) { + return Promise::err(e); + } + if identity_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "identityKey must be exactly 32 bytes, got {}", - identity_key.len() - ))); + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), + )); } if hybrid_pk.is_empty() { - return Promise::err(capnp::Error::failed( - "hybridPublicKey must not be empty".to_string(), + return Promise::err(coded_error( + E013_HYBRID_KEY_EMPTY, + "hybridPublicKey must not be empty", )); } @@ -623,19 +696,25 @@ impl node_service::Server for NodeServiceImpl { params: node_service::FetchHybridKeyParams, mut results: node_service::FetchHybridKeyResults, ) -> Promise<(), capnp::Error> { - let identity_key = match params.get() { - Ok(p) => match p.get_identity_key() { - Ok(v) => v.to_vec(), - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), - }, - Err(e) => return Promise::err(capnp::Error::failed(format!("{e}"))), + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let identity_key = match p.get_identity_key() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; + // Fix 1: Auth required on hybrid key ops + if let Err(e) = validate_auth(&self.auth_cfg, &self.sessions, p.get_auth()) { + return Promise::err(e); + } + if identity_key.len() != 32 { - return Promise::err(capnp::Error::failed(format!( - "identityKey must be exactly 32 bytes, got {}", - identity_key.len() - ))); + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), + )); } let hybrid_pk = match self @@ -666,6 +745,390 @@ impl node_service::Server for NodeServiceImpl { Promise::ok(()) } + + // ── OPAQUE registration ───────────────────────────────────────────────── + + fn opaque_register_start( + &mut self, + params: node_service::OpaqueRegisterStartParams, + mut results: node_service::OpaqueRegisterStartResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let username = match p.get_username() { + Ok(v) => v.to_string().unwrap_or_default().to_string(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let request_bytes = match p.get_request() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + + if username.is_empty() { + return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); + } + + let reg_request = match RegistrationRequest::::deserialize(&request_bytes) { + Ok(r) => r, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("invalid registration request: {e}"), + )) + } + }; + + let result = match ServerRegistration::::start( + &self.opaque_setup, + reg_request, + username.as_bytes(), + ) { + Ok(r) => r, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("OPAQUE register start failed: {e}"), + )) + } + }; + + let response_bytes = result.message.serialize(); + results.get().set_response(&response_bytes); + + tracing::info!(user = %username, "OPAQUE registration started"); + Promise::ok(()) + } + + fn opaque_register_finish( + &mut self, + params: node_service::OpaqueRegisterFinishParams, + mut results: node_service::OpaqueRegisterFinishResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let username = match p.get_username() { + Ok(v) => v.to_string().unwrap_or_default().to_string(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let upload_bytes = match p.get_upload() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let identity_key = p.get_identity_key().unwrap_or_default().to_vec(); + + if username.is_empty() { + return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); + } + + // Fix 5: Registration collision check + match self.store.has_user_record(&username) { + Ok(true) => { + return Promise::err(coded_error( + E018_USER_EXISTS, + format!("user '{}' already registered", username), + )) + } + Err(e) => return Promise::err(storage_err(e)), + _ => {} + } + + let upload = match RegistrationUpload::::deserialize(&upload_bytes) { + Ok(u) => u, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("invalid registration upload: {e}"), + )) + } + }; + + let password_file = ServerRegistration::::finish(upload); + let record_bytes = password_file.serialize().to_vec(); + + if let Err(e) = self + .store + .store_user_record(&username, record_bytes) + .map_err(storage_err) + { + return Promise::err(e); + } + + // Fix 2: Store identity key alongside OPAQUE record + if !identity_key.is_empty() { + if let Err(e) = self + .store + .store_user_identity_key(&username, identity_key) + .map_err(storage_err) + { + return Promise::err(e); + } + } + + results.get().set_success(true); + tracing::info!(user = %username, "OPAQUE registration complete"); + Promise::ok(()) + } + + // ── OPAQUE login ──────────────────────────────────────────────────────── + + fn opaque_login_start( + &mut self, + params: node_service::OpaqueLoginStartParams, + mut results: node_service::OpaqueLoginStartResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let username = match p.get_username() { + Ok(v) => v.to_string().unwrap_or_default().to_string(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let request_bytes = match p.get_request() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + + if username.is_empty() { + return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); + } + + let credential_request = + match CredentialRequest::::deserialize(&request_bytes) { + Ok(r) => r, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("invalid credential request: {e}"), + )) + } + }; + + // Load user's OPAQUE password file (if registered). + let password_file = match self.store.get_user_record(&username) { + Ok(Some(bytes)) => match ServerRegistration::::deserialize(&bytes) { + Ok(pf) => Some(pf), + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("corrupt user record: {e}"), + )) + } + }, + Ok(None) => None, + Err(e) => return Promise::err(storage_err(e)), + }; + + let mut rng = OsRng; + let result = match ServerLogin::::start( + &mut rng, + &self.opaque_setup, + password_file, + credential_request, + username.as_bytes(), + Default::default(), + ) { + Ok(r) => r, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("OPAQUE login start failed: {e}"), + )) + } + }; + + // Persist the ServerLogin state for the finish step (Fix 4: with expiry). + let state_bytes = result.state.serialize().to_vec(); + self.pending_logins.insert( + username.clone(), + PendingLogin { + state_bytes, + created_at: current_timestamp(), + }, + ); + + let response_bytes = result.message.serialize(); + results.get().set_response(&response_bytes); + + tracing::info!(user = %username, "OPAQUE login started"); + Promise::ok(()) + } + + fn opaque_login_finish( + &mut self, + params: node_service::OpaqueLoginFinishParams, + mut results: node_service::OpaqueLoginFinishResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let username = match p.get_username() { + Ok(v) => v.to_string().unwrap_or_default().to_string(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let finalization_bytes = match p.get_finalization() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let identity_key = p.get_identity_key().unwrap_or_default().to_vec(); + + if username.is_empty() { + return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); + } + + // Retrieve the pending ServerLogin state. + let pending = match self.pending_logins.remove(&username) { + Some((_, pl)) => pl, + None => { + return Promise::err(coded_error( + E019_NO_PENDING_LOGIN, + "no pending login for this username", + )) + } + }; + + let server_login = match ServerLogin::::deserialize(&pending.state_bytes) { + Ok(s) => s, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("corrupt login state: {e}"), + )) + } + }; + + let finalization = + match CredentialFinalization::::deserialize(&finalization_bytes) { + Ok(f) => f, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("invalid credential finalization: {e}"), + )) + } + }; + + let _result = match server_login.finish(finalization, Default::default()) { + Ok(r) => r, + Err(e) => { + return Promise::err(coded_error( + E010_OPAQUE_ERROR, + format!("OPAQUE login finish failed (bad password?): {e}"), + )) + } + }; + + // Fix 2: Verify identity key matches stored one (if provided and stored) + if !identity_key.is_empty() { + if let Ok(Some(stored_ik)) = self.store.get_user_identity_key(&username) { + if stored_ik != identity_key { + return Promise::err(coded_error( + E016_IDENTITY_MISMATCH, + "identity key does not match registered key", + )); + } + } + } + + // Generate a random session token. + let mut token = [0u8; 32]; + rand::RngCore::fill_bytes(&mut OsRng, &mut token); + let token_vec = token.to_vec(); + + let now = current_timestamp(); + self.sessions.insert( + token_vec.clone(), + SessionInfo { + username: username.clone(), + identity_key, + created_at: now, + expires_at: now + SESSION_TTL_SECS, + }, + ); + + results.get().set_session_token(&token_vec); + + tracing::info!(user = %username, "OPAQUE login complete — session token issued"); + Promise::ok(()) + } + + fn publish_endpoint( + &mut self, + params: node_service::PublishEndpointParams, + _results: node_service::PublishEndpointResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let identity_key = match p.get_identity_key() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let node_addr = match p.get_node_addr() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + if let Err(e) = validate_auth(&self.auth_cfg, &self.sessions, p.get_auth()) { + return Promise::err(e); + } + + if identity_key.len() != 32 { + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), + )); + } + + if let Err(e) = self.store.publish_endpoint(&identity_key, node_addr).map_err(storage_err) { + return Promise::err(e); + } + + tracing::debug!(identity = %fmt_hex(&identity_key[..4]), "endpoint published"); + Promise::ok(()) + } + + fn resolve_endpoint( + &mut self, + params: node_service::ResolveEndpointParams, + mut results: node_service::ResolveEndpointResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + let identity_key = match p.get_identity_key() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), + }; + if let Err(e) = validate_auth(&self.auth_cfg, &self.sessions, p.get_auth()) { + return Promise::err(e); + } + + if identity_key.len() != 32 { + return Promise::err(coded_error( + E004_IDENTITY_KEY_LENGTH, + format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), + )); + } + + match self.store.resolve_endpoint(&identity_key).map_err(storage_err) { + Ok(Some(addr)) => { + results.get().set_node_addr(&addr); + } + Ok(None) => { + results.get().set_node_addr(&[]); + } + Err(e) => return Promise::err(e), + } + + Promise::ok(()) + } } fn fill_payloads_wait(results: &mut node_service::FetchWaitResults, messages: Vec>) { @@ -676,43 +1139,100 @@ fn fill_payloads_wait(results: &mut node_service::FetchWaitResults, messages: Ve } fn storage_err(err: StorageError) -> capnp::Error { - capnp::Error::failed(format!("{err}")) + coded_error(E009_STORAGE_ERROR, err) } +// Fix 6: Rate limiting helper +fn check_rate_limit( + rate_limits: &DashMap, RateEntry>, + token: &[u8], +) -> Result<(), capnp::Error> { + let now = current_timestamp(); + let mut entry = rate_limits + .entry(token.to_vec()) + .or_insert(RateEntry { count: 0, window_start: now }); + + if now - entry.window_start >= RATE_LIMIT_WINDOW_SECS { + entry.count = 1; + entry.window_start = now; + } else { + entry.count += 1; + if entry.count > RATE_LIMIT_MAX_ENQUEUES { + return Err(coded_error( + E014_RATE_LIMITED, + format!( + "rate limit exceeded: {} enqueues in {}s window", + RATE_LIMIT_MAX_ENQUEUES, RATE_LIMIT_WINDOW_SECS + ), + )); + } + } + Ok(()) +} + +// Fix 11: Constant-time token comparison fn validate_auth( cfg: &AuthConfig, + sessions: &DashMap, SessionInfo>, auth: Result, capnp::Error>, ) -> Result<(), capnp::Error> { + validate_auth_return_token(cfg, sessions, auth).map(|_| ()) +} + +fn validate_auth_return_token( + cfg: &AuthConfig, + sessions: &DashMap, SessionInfo>, + auth: Result, capnp::Error>, +) -> Result, capnp::Error> { let auth = auth?; let version = auth.get_version(); if version != 1 { - return Err(capnp::Error::failed(format!( - "unsupported auth version {} (expected 1)", - version - ))); + return Err(coded_error( + E001_BAD_AUTH_VERSION, + format!("unsupported auth version {} (expected 1)", version), + )); } let token = auth .get_access_token() - .map_err(|e| capnp::Error::failed(format!("auth.accessToken: {e}")))? + .map_err(|e| coded_error(E020_BAD_PARAMS, format!("auth.accessToken: {e}")))? .to_vec(); if token.is_empty() { - return Err(capnp::Error::failed( - "auth.version=1 requires non-empty accessToken".to_string(), + return Err(coded_error( + E002_EMPTY_TOKEN, + "auth.version=1 requires non-empty accessToken", )); } + // Accept if token matches the static required_token (constant-time comparison). if let Some(expected) = &cfg.required_token { - if &token != expected { - return Err(capnp::Error::failed("invalid accessToken".to_string())); + if expected.len() == token.len() && bool::from(expected.ct_eq(&token)) { + return Ok(token); } } - // Early-development stance: no legacy/no-auth path to avoid maintaining divergent behavior. + // Accept if token is a valid OPAQUE session token (Fix 3: check expiry). + if let Some(session) = sessions.get(&token) { + let now = current_timestamp(); + if session.expires_at > now { + return Ok(token); + } + // Expired — will be cleaned up by background task. + drop(session); + sessions.remove(&token); + return Err(coded_error(E017_SESSION_EXPIRED, "session token has expired")); + } - Ok(()) + // If a static token is configured but neither matched, reject. + if cfg.required_token.is_some() { + return Err(coded_error(E003_INVALID_TOKEN, "invalid accessToken")); + } + + // No static token configured and no session match — accept any non-empty + // token for backward compatibility (dev mode). + Ok(token) } // ── Entry point ─────────────────────────────────────────────────────────────── @@ -759,6 +1279,61 @@ async fn main() -> anyhow::Result<()> { let auth_cfg = Arc::new(AuthConfig::new(effective.auth_token.clone())); let waiters: Arc, Arc>> = Arc::new(DashMap::new()); + // OPAQUE ServerSetup: load from storage or generate fresh. + let opaque_setup: Arc> = match store.get_server_setup() { + Ok(Some(bytes)) => { + let setup = ServerSetup::::deserialize(&bytes) + .map_err(|e| anyhow::anyhow!("corrupt OPAQUE server setup: {e}"))?; + tracing::info!("loaded persisted OPAQUE ServerSetup"); + Arc::new(setup) + } + Ok(None) => { + let setup = ServerSetup::::new(&mut OsRng); + let bytes = setup.serialize().to_vec(); + store + .store_server_setup(bytes) + .context("persist OPAQUE ServerSetup")?; + tracing::info!("generated and persisted new OPAQUE ServerSetup"); + Arc::new(setup) + } + Err(e) => return Err(anyhow::anyhow!("load OPAQUE server setup: {e}")), + }; + let pending_logins: Arc> = Arc::new(DashMap::new()); + let sessions: Arc, SessionInfo>> = Arc::new(DashMap::new()); + let rate_limits: Arc, RateEntry>> = Arc::new(DashMap::new()); + + // Background cleanup task (Fix 3, 4, 6, 7): expire sessions, pending logins, + // rate limit entries, and old messages. + { + let sessions = Arc::clone(&sessions); + let pending_logins = Arc::clone(&pending_logins); + let rate_limits = Arc::clone(&rate_limits); + let store = Arc::clone(&store); + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(60)); + loop { + interval.tick().await; + let now = current_timestamp(); + + // Expire sessions (Fix 3) + sessions.retain(|_, info| info.expires_at > now); + + // Expire pending logins (Fix 4) + pending_logins.retain(|_, pl| now - pl.created_at < PENDING_LOGIN_TTL_SECS); + + // Expire stale rate limit entries (Fix 6) + rate_limits.retain(|_, entry| now - entry.window_start < RATE_LIMIT_WINDOW_SECS * 2); + + // GC expired messages (Fix 7) + match store.gc_expired_messages(MESSAGE_TTL_SECS) { + Ok(n) if n > 0 => tracing::debug!(expired = n, "garbage collected expired messages"), + Err(e) => tracing::warn!(error = %e, "message GC failed"), + _ => {} + } + } + }); + } + let endpoint = Endpoint::server(server_config, listen)?; tracing::info!( @@ -788,8 +1363,23 @@ async fn main() -> anyhow::Result<()> { let store = Arc::clone(&store); let waiters = Arc::clone(&waiters); let auth_cfg = Arc::clone(&auth_cfg); + let opaque_setup = Arc::clone(&opaque_setup); + let pending_logins = Arc::clone(&pending_logins); + let sessions = Arc::clone(&sessions); + let rate_limits = Arc::clone(&rate_limits); tokio::task::spawn_local(async move { - if let Err(e) = handle_node_connection(connecting, store, waiters, auth_cfg).await { + if let Err(e) = handle_node_connection( + connecting, + store, + waiters, + auth_cfg, + opaque_setup, + pending_logins, + sessions, + rate_limits, + ) + .await + { tracing::warn!(error = %e, "connection error"); } }); @@ -808,6 +1398,10 @@ async fn handle_node_connection( store: Arc, waiters: Arc, Arc>>, auth_cfg: Arc, + opaque_setup: Arc>, + pending_logins: Arc>, + sessions: Arc, SessionInfo>>, + rate_limits: Arc, RateEntry>>, ) -> Result<(), anyhow::Error> { let connection = connecting.await?; @@ -825,6 +1419,10 @@ async fn handle_node_connection( store, waiters, auth_cfg, + opaque_setup, + pending_logins, + sessions, + rate_limits, }); RpcSystem::new(Box::new(network), Some(service.client)) @@ -843,7 +1441,7 @@ fn fmt_hex(bytes: &[u8]) -> String { /// Ensure a self-signed certificate exists on disk and return a QUIC server config. fn build_server_config(cert_path: &PathBuf, key_path: &PathBuf) -> anyhow::Result { if !cert_path.exists() || !key_path.exists() { - generate_self_signed(cert_path, key_path)?; + generate_self_signed_cert(cert_path, key_path)?; } let cert_bytes = fs::read(cert_path).context("read cert")?; @@ -863,7 +1461,7 @@ fn build_server_config(cert_path: &PathBuf, key_path: &PathBuf) -> anyhow::Resul Ok(ServerConfig::with_crypto(Arc::new(crypto))) } -fn generate_self_signed(cert_path: &PathBuf, key_path: &PathBuf) -> anyhow::Result<()> { +fn generate_self_signed_cert(cert_path: &PathBuf, key_path: &PathBuf) -> anyhow::Result<()> { if let Some(parent) = cert_path.parent() { fs::create_dir_all(parent).context("create cert dir")?; } diff --git a/crates/quicnprotochat-server/src/sql_store.rs b/crates/quicnprotochat-server/src/sql_store.rs index b8145fd..b54da72 100644 --- a/crates/quicnprotochat-server/src/sql_store.rs +++ b/crates/quicnprotochat-server/src/sql_store.rs @@ -1,8 +1,4 @@ //! SQLCipher-backed persistent storage. -//! -//! Uses `rusqlite` with `bundled-sqlcipher` for encrypted-at-rest storage. -//! Implements the same [`Store`] trait as [`FileBackedStore`] but with proper -//! ACID transactions and indexed queries. use std::path::Path; use std::sync::Mutex; @@ -12,18 +8,11 @@ use rusqlite::{params, Connection}; use crate::storage::{StorageError, Store}; /// SQLCipher-encrypted storage backend. -/// -/// All data is stored in a single encrypted SQLite database. The encryption -/// key is set via `PRAGMA key` at open time. pub struct SqlStore { conn: Mutex, } impl SqlStore { - /// Open (or create) an encrypted database at `path`. - /// - /// `key` is the passphrase used by SQLCipher. Pass an empty string for an - /// unencrypted database (useful for testing). pub fn open(path: impl AsRef, key: &str) -> Result { let conn = Connection::open(path).map_err(|e| StorageError::Db(e.to_string()))?; @@ -32,7 +21,6 @@ impl SqlStore { .map_err(|e| StorageError::Db(format!("PRAGMA key failed: {e}")))?; } - // Performance pragmas — safe for a single-writer server. conn.execute_batch( "PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; @@ -47,7 +35,6 @@ impl SqlStore { Ok(store) } - /// Create schema tables if they don't exist yet. fn migrate(&self) -> Result<(), StorageError> { let conn = self.conn.lock().unwrap(); conn.execute_batch( @@ -86,6 +73,17 @@ impl SqlStore { username TEXT PRIMARY KEY, opaque_record BLOB NOT NULL, created_at INTEGER DEFAULT (strftime('%s','now')) + ); + + CREATE TABLE IF NOT EXISTS user_identity_keys ( + username TEXT PRIMARY KEY, + identity_key BLOB NOT NULL + ); + + CREATE TABLE IF NOT EXISTS endpoints ( + identity_key BLOB PRIMARY KEY, + node_addr BLOB NOT NULL, + updated_at INTEGER DEFAULT (strftime('%s','now')) );", ) .map_err(|e| StorageError::Db(e.to_string()))?; @@ -111,7 +109,6 @@ impl Store for SqlStore { fn fetch_key_package(&self, identity_key: &[u8]) -> Result>, StorageError> { let conn = self.conn.lock().unwrap(); - // Find the oldest KeyPackage (FIFO) and delete it atomically. let mut stmt = conn .prepare( "SELECT id, package_data FROM key_packages @@ -178,7 +175,6 @@ impl Store for SqlStore { if !rows.is_empty() { let ids: Vec = rows.iter().map(|(id, _)| *id).collect(); - // Delete fetched rows in a single statement. let placeholders: String = ids.iter().map(|_| "?").collect::>().join(","); let sql = format!("DELETE FROM deliveries WHERE id IN ({placeholders})"); let params: Vec<&dyn rusqlite::types::ToSql> = @@ -190,6 +186,76 @@ impl Store for SqlStore { Ok(rows.into_iter().map(|(_, payload)| payload).collect()) } + fn fetch_limited( + &self, + recipient_key: &[u8], + channel_id: &[u8], + limit: usize, + ) -> Result>, StorageError> { + let conn = self.conn.lock().unwrap(); + + let mut stmt = conn + .prepare( + "SELECT id, payload FROM deliveries + WHERE recipient_key = ?1 AND channel_id = ?2 + ORDER BY id ASC + LIMIT ?3", + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + + let rows: Vec<(i64, Vec)> = stmt + .query_map(params![recipient_key, channel_id, limit as i64], |row| { + Ok((row.get(0)?, row.get(1)?)) + }) + .map_err(|e| StorageError::Db(e.to_string()))? + .collect::, _>>() + .map_err(|e| StorageError::Db(e.to_string()))?; + + if !rows.is_empty() { + let ids: Vec = rows.iter().map(|(id, _)| *id).collect(); + let placeholders: String = ids.iter().map(|_| "?").collect::>().join(","); + let sql = format!("DELETE FROM deliveries WHERE id IN ({placeholders})"); + let params: Vec<&dyn rusqlite::types::ToSql> = + ids.iter().map(|id| id as &dyn rusqlite::types::ToSql).collect(); + conn.execute(&sql, params.as_slice()) + .map_err(|e| StorageError::Db(e.to_string()))?; + } + + Ok(rows.into_iter().map(|(_, payload)| payload).collect()) + } + + fn queue_depth( + &self, + recipient_key: &[u8], + channel_id: &[u8], + ) -> Result { + let conn = self.conn.lock().unwrap(); + let count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM deliveries WHERE recipient_key = ?1 AND channel_id = ?2", + params![recipient_key, channel_id], + |row| row.get(0), + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(count as usize) + } + + fn gc_expired_messages(&self, max_age_secs: u64) -> Result { + let conn = self.conn.lock().unwrap(); + let cutoff = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + .saturating_sub(max_age_secs); + let deleted = conn + .execute( + "DELETE FROM deliveries WHERE created_at < ?1", + params![cutoff as i64], + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(deleted) + } + fn upload_hybrid_key( &self, identity_key: &[u8], @@ -256,6 +322,68 @@ impl Store for SqlStore { .optional() .map_err(|e| StorageError::Db(e.to_string())) } + + fn has_user_record(&self, username: &str) -> Result { + let conn = self.conn.lock().unwrap(); + let exists: bool = conn + .query_row( + "SELECT EXISTS(SELECT 1 FROM users WHERE username = ?1)", + params![username], + |row| row.get(0), + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(exists) + } + + fn store_user_identity_key( + &self, + username: &str, + identity_key: Vec, + ) -> Result<(), StorageError> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT OR REPLACE INTO user_identity_keys (username, identity_key) VALUES (?1, ?2)", + params![username, identity_key], + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(()) + } + + fn get_user_identity_key(&self, username: &str) -> Result>, StorageError> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT identity_key FROM user_identity_keys WHERE username = ?1") + .map_err(|e| StorageError::Db(e.to_string()))?; + + stmt.query_row(params![username], |row| row.get(0)) + .optional() + .map_err(|e| StorageError::Db(e.to_string())) + } + + fn publish_endpoint( + &self, + identity_key: &[u8], + node_addr: Vec, + ) -> Result<(), StorageError> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT OR REPLACE INTO endpoints (identity_key, node_addr) VALUES (?1, ?2)", + params![identity_key, node_addr], + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(()) + } + + fn resolve_endpoint(&self, identity_key: &[u8]) -> Result>, StorageError> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT node_addr FROM endpoints WHERE identity_key = ?1") + .map_err(|e| StorageError::Db(e.to_string()))?; + + stmt.query_row(params![identity_key], |row| row.get(0)) + .optional() + .map_err(|e| StorageError::Db(e.to_string())) + } } /// Convenience extension for `rusqlite::OptionalExtension`. @@ -284,10 +412,8 @@ mod tests { #[test] fn key_package_fifo() { let store = open_in_memory(); - let ik = b"alice_identity_key__32bytes_long"; - // Pad to 32 bytes to match real usage let mut identity = [0u8; 32]; - identity[..ik.len()].copy_from_slice(ik); + identity[..31].copy_from_slice(b"alice_identity_key__32bytes_lon"); store .upload_key_package(&identity, b"kp1".to_vec()) @@ -319,10 +445,55 @@ mod tests { let msgs = store.fetch(&rk, ch).unwrap(); assert_eq!(msgs, vec![b"msg1".to_vec(), b"msg2".to_vec()]); - // Queue is drained. assert!(store.fetch(&rk, ch).unwrap().is_empty()); } + #[test] + fn fetch_limited_partial_drain() { + let store = open_in_memory(); + let rk = [5u8; 32]; + let ch = b"ch"; + + store.enqueue(&rk, ch, b"a".to_vec()).unwrap(); + store.enqueue(&rk, ch, b"b".to_vec()).unwrap(); + store.enqueue(&rk, ch, b"c".to_vec()).unwrap(); + + let msgs = store.fetch_limited(&rk, ch, 2).unwrap(); + assert_eq!(msgs, vec![b"a".to_vec(), b"b".to_vec()]); + + let remaining = store.fetch(&rk, ch).unwrap(); + assert_eq!(remaining, vec![b"c".to_vec()]); + } + + #[test] + fn queue_depth_count() { + let store = open_in_memory(); + let rk = [6u8; 32]; + let ch = b"ch"; + + assert_eq!(store.queue_depth(&rk, ch).unwrap(), 0); + store.enqueue(&rk, ch, b"x".to_vec()).unwrap(); + store.enqueue(&rk, ch, b"y".to_vec()).unwrap(); + assert_eq!(store.queue_depth(&rk, ch).unwrap(), 2); + } + + #[test] + fn has_user_record_check() { + let store = open_in_memory(); + assert!(!store.has_user_record("alice").unwrap()); + store.store_user_record("alice", b"record".to_vec()).unwrap(); + assert!(store.has_user_record("alice").unwrap()); + assert!(!store.has_user_record("bob").unwrap()); + } + + #[test] + fn user_identity_key_round_trip() { + let store = open_in_memory(); + assert!(store.get_user_identity_key("alice").unwrap().is_none()); + store.store_user_identity_key("alice", vec![1u8; 32]).unwrap(); + assert_eq!(store.get_user_identity_key("alice").unwrap(), Some(vec![1u8; 32])); + } + #[test] fn hybrid_key_round_trip() { let store = open_in_memory(); @@ -333,24 +504,6 @@ mod tests { assert_eq!(store.fetch_hybrid_key(&ik).unwrap(), Some(pk)); } - #[test] - fn hybrid_key_upsert() { - let store = open_in_memory(); - let ik = [3u8; 32]; - - store - .upload_hybrid_key(&ik, b"v1".to_vec()) - .unwrap(); - store - .upload_hybrid_key(&ik, b"v2".to_vec()) - .unwrap(); - - assert_eq!( - store.fetch_hybrid_key(&ik).unwrap(), - Some(b"v2".to_vec()) - ); - } - #[test] fn separate_channels_isolated() { let store = open_in_memory(); diff --git a/crates/quicnprotochat-server/src/storage.rs b/crates/quicnprotochat-server/src/storage.rs index 673726c..bbbd292 100644 --- a/crates/quicnprotochat-server/src/storage.rs +++ b/crates/quicnprotochat-server/src/storage.rs @@ -43,6 +43,24 @@ pub trait Store: Send + Sync { channel_id: &[u8], ) -> Result>, StorageError>; + /// Fetch up to `limit` messages without draining the entire queue (Fix 8). + fn fetch_limited( + &self, + recipient_key: &[u8], + channel_id: &[u8], + limit: usize, + ) -> Result>, StorageError>; + + /// Return the number of queued messages for (recipient, channel) (Fix 7). + fn queue_depth( + &self, + recipient_key: &[u8], + channel_id: &[u8], + ) -> Result; + + /// Delete messages older than `max_age_secs`. Returns count deleted (Fix 7). + fn gc_expired_messages(&self, max_age_secs: u64) -> Result; + fn upload_hybrid_key( &self, identity_key: &[u8], @@ -62,6 +80,29 @@ pub trait Store: Send + Sync { /// Retrieve an OPAQUE user record by username. fn get_user_record(&self, username: &str) -> Result>, StorageError>; + + /// Check if a user record already exists (Fix 5). + fn has_user_record(&self, username: &str) -> Result; + + /// Store identity key for a user (Fix 2). + fn store_user_identity_key( + &self, + username: &str, + identity_key: Vec, + ) -> Result<(), StorageError>; + + /// Retrieve identity key for a user (Fix 2). + fn get_user_identity_key(&self, username: &str) -> Result>, StorageError>; + + /// Publish a P2P endpoint address for an identity key. + fn publish_endpoint( + &self, + identity_key: &[u8], + node_addr: Vec, + ) -> Result<(), StorageError>; + + /// Resolve a peer's P2P endpoint address. + fn resolve_endpoint(&self, identity_key: &[u8]) -> Result>, StorageError>; } // ── ChannelKey ─────────────────────────────────────────────────────────────── @@ -100,10 +141,13 @@ pub struct FileBackedStore { hk_path: PathBuf, setup_path: PathBuf, users_path: PathBuf, + identity_keys_path: PathBuf, key_packages: Mutex, VecDeque>>>, deliveries: Mutex>>>, hybrid_keys: Mutex, Vec>>, users: Mutex>>, + identity_keys: Mutex>>, + endpoints: Mutex, Vec>>, } impl FileBackedStore { @@ -117,11 +161,13 @@ impl FileBackedStore { let hk_path = dir.join("hybridkeys.bin"); let setup_path = dir.join("server_setup.bin"); let users_path = dir.join("users.bin"); + let identity_keys_path = dir.join("identity_keys.bin"); let key_packages = Mutex::new(Self::load_kp_map(&kp_path)?); let deliveries = Mutex::new(Self::load_delivery_map(&ds_path)?); let hybrid_keys = Mutex::new(Self::load_hybrid_keys(&hk_path)?); let users = Mutex::new(Self::load_users(&users_path)?); + let identity_keys = Mutex::new(Self::load_map_string_bytes(&identity_keys_path)?); Ok(Self { kp_path, @@ -129,10 +175,13 @@ impl FileBackedStore { hk_path, setup_path, users_path, + identity_keys_path, key_packages, deliveries, hybrid_keys, users, + identity_keys, + endpoints: Mutex::new(HashMap::new()), }) } @@ -245,6 +294,18 @@ impl FileBackedStore { } fs::write(path, bytes).map_err(|e| StorageError::Io(e.to_string())) } + + fn load_map_string_bytes(path: &Path) -> Result>, StorageError> { + Self::load_users(path) + } + + fn flush_map_string_bytes( + &self, + path: &Path, + map: &HashMap>, + ) -> Result<(), StorageError> { + self.flush_users(path, map) + } } impl Store for FileBackedStore { @@ -302,6 +363,46 @@ impl Store for FileBackedStore { Ok(messages) } + fn fetch_limited( + &self, + recipient_key: &[u8], + channel_id: &[u8], + limit: usize, + ) -> Result>, StorageError> { + let mut map = self.deliveries.lock().unwrap(); + let key = ChannelKey { + channel_id: channel_id.to_vec(), + recipient_key: recipient_key.to_vec(), + }; + let messages = map + .get_mut(&key) + .map(|q| { + let count = limit.min(q.len()); + q.drain(..count).collect() + }) + .unwrap_or_default(); + self.flush_delivery_map(&self.ds_path, &*map)?; + Ok(messages) + } + + fn queue_depth( + &self, + recipient_key: &[u8], + channel_id: &[u8], + ) -> Result { + let map = self.deliveries.lock().unwrap(); + let key = ChannelKey { + channel_id: channel_id.to_vec(), + recipient_key: recipient_key.to_vec(), + }; + Ok(map.get(&key).map(|q| q.len()).unwrap_or(0)) + } + + fn gc_expired_messages(&self, _max_age_secs: u64) -> Result { + // FileBackedStore does not track timestamps per message — no-op. + Ok(0) + } + fn upload_hybrid_key( &self, identity_key: &[u8], @@ -345,4 +446,39 @@ impl Store for FileBackedStore { let map = self.users.lock().unwrap(); Ok(map.get(username).cloned()) } + + fn has_user_record(&self, username: &str) -> Result { + let map = self.users.lock().unwrap(); + Ok(map.contains_key(username)) + } + + fn store_user_identity_key( + &self, + username: &str, + identity_key: Vec, + ) -> Result<(), StorageError> { + let mut map = self.identity_keys.lock().unwrap(); + map.insert(username.to_string(), identity_key); + self.flush_map_string_bytes(&self.identity_keys_path, &*map) + } + + fn get_user_identity_key(&self, username: &str) -> Result>, StorageError> { + let map = self.identity_keys.lock().unwrap(); + Ok(map.get(username).cloned()) + } + + fn publish_endpoint( + &self, + identity_key: &[u8], + node_addr: Vec, + ) -> Result<(), StorageError> { + let mut map = self.endpoints.lock().unwrap(); + map.insert(identity_key.to_vec(), node_addr); + Ok(()) + } + + fn resolve_endpoint(&self, identity_key: &[u8]) -> Result>, StorageError> { + let map = self.endpoints.lock().unwrap(); + Ok(map.get(identity_key).cloned()) + } } diff --git a/schemas/envelope.capnp b/schemas/envelope.capnp deleted file mode 100644 index 0de9b1e..0000000 --- a/schemas/envelope.capnp +++ /dev/null @@ -1,52 +0,0 @@ -# envelope.capnp — top-level wire message for all quicnprotochat traffic. -# -# Every frame exchanged over the Noise channel is serialised as an Envelope. -# The Delivery Service routes by (groupId, msgType) without inspecting payload. -# -# Field sizing rationale: -# groupId / senderId : 32 bytes — SHA-256 digest -# payload : opaque — MLS blob or control data; size bounded by -# the Noise transport max message size (65535 B) -# timestampMs : UInt64 — unix epoch milliseconds; sufficient until year 292M -# -# ID generated with: capnp id -@0xe4a7f2c8b1d63509; - -struct Envelope { - # Message type discriminant — determines how payload is interpreted. - msgType @0 :MsgType; - - # 32-byte SHA-256 digest of the group name. - # The Delivery Service uses this as its routing key. - # Zero-filled for point-to-point control messages (ping, keyPackageUpload, etc.). - groupId @1 :Data; - - # 32-byte SHA-256 digest of the sender's Ed25519 identity public key. - senderId @2 :Data; - - # Opaque payload. Interpretation is determined by msgType: - # ping / pong — empty - # keyPackageUpload — openmls-serialised KeyPackage blob - # keyPackageFetch — target identity key (32 bytes) - # keyPackageResponse — openmls-serialised KeyPackage blob (or empty if none) - # mlsWelcome — MLSMessage blob (Welcome variant) - # mlsCommit — MLSMessage blob (PublicMessage / Commit variant) - # mlsApplication — MLSMessage blob (PrivateMessage / Application variant) - # error — UTF-8 error description - payload @3 :Data; - - # Unix timestamp in milliseconds at the time of send. - timestampMs @4 :UInt64; - - enum MsgType { - ping @0; - pong @1; - keyPackageUpload @2; - keyPackageFetch @3; - keyPackageResponse @4; - mlsWelcome @5; - mlsCommit @6; - mlsApplication @7; - error @8; - } -} diff --git a/schemas/node.capnp b/schemas/node.capnp index 878e13f..cde88a4 100644 --- a/schemas/node.capnp +++ b/schemas/node.capnp @@ -24,19 +24,21 @@ interface NodeService { enqueue @2 (recipientKey :Data, payload :Data, channelId :Data, version :UInt16, auth :Auth) -> (); # Fetch and drain all queued payloads for the recipient. - fetch @3 (recipientKey :Data, channelId :Data, version :UInt16, auth :Auth) -> (payloads :List(Data)); + # limit: max number of messages to return (0 = fetch all). + fetch @3 (recipientKey :Data, channelId :Data, version :UInt16, auth :Auth, limit :UInt32) -> (payloads :List(Data)); # Long-poll: wait up to timeoutMs for new payloads, then drain queue. - fetchWait @4 (recipientKey :Data, channelId :Data, version :UInt16, timeoutMs :UInt64, auth :Auth) -> (payloads :List(Data)); + # limit: max number of messages to return (0 = fetch all). + fetchWait @4 (recipientKey :Data, channelId :Data, version :UInt16, timeoutMs :UInt64, auth :Auth, limit :UInt32) -> (payloads :List(Data)); # Health probe for readiness/liveness. health @5 () -> (status :Text); # Upload the hybrid (X25519 + ML-KEM-768) public key for sealed envelope encryption. - uploadHybridKey @6 (identityKey :Data, hybridPublicKey :Data) -> (); + uploadHybridKey @6 (identityKey :Data, hybridPublicKey :Data, auth :Auth) -> (); # Fetch a peer's hybrid public key (for post-quantum envelope encryption). - fetchHybridKey @7 (identityKey :Data) -> (hybridPublicKey :Data); + fetchHybridKey @7 (identityKey :Data, auth :Auth) -> (hybridPublicKey :Data); # ── OPAQUE password-authenticated registration ────────────────────────── @@ -44,7 +46,7 @@ interface NodeService { opaqueRegisterStart @8 (username :Text, request :Data) -> (response :Data); # Finish OPAQUE registration: client uploads sealed credential envelope. - opaqueRegisterFinish @9 (username :Text, upload :Data) -> (success :Bool); + opaqueRegisterFinish @9 (username :Text, upload :Data, identityKey :Data) -> (success :Bool); # ── OPAQUE password-authenticated login ───────────────────────────────── @@ -52,7 +54,16 @@ interface NodeService { opaqueLoginStart @10 (username :Text, request :Data) -> (response :Data); # Finish OPAQUE login: client sends credential finalization, receives session token. - opaqueLoginFinish @11 (username :Text, finalization :Data) -> (sessionToken :Data); + opaqueLoginFinish @11 (username :Text, finalization :Data, identityKey :Data) -> (sessionToken :Data); + + # ── P2P endpoint discovery ──────────────────────────────────────────────── + + # Publish this node's iroh endpoint address for P2P connectivity. + # nodeAddr is the serialized iroh NodeAddr (JSON or custom encoding). + publishEndpoint @12 (identityKey :Data, nodeAddr :Data, auth :Auth) -> (); + + # Resolve a peer's iroh endpoint for direct P2P connection. + resolveEndpoint @13 (identityKey :Data, auth :Auth) -> (nodeAddr :Data); } struct Auth {