diff --git a/Cargo.lock b/Cargo.lock index 67abec7..4e39c40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,30 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aead" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" -dependencies = [ - "generic-array", -] - [[package]] name = "aead" version = "0.5.2" @@ -36,18 +12,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if", - "cipher 0.3.0", - "cpufeatures", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.4" @@ -55,35 +19,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] -[[package]] -name = "aes-gcm" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" -dependencies = [ - "aead 0.4.3", - "aes 0.7.5", - "cipher 0.3.0", - "ctr 0.7.0", - "ghash 0.4.4", - "subtle", -] - [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.1", + "aead", + "aes", + "cipher", + "ctr", + "ghash", "subtle", ] @@ -364,21 +314,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -447,15 +382,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -608,18 +534,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" -dependencies = [ - "cfg-if", - "cipher 0.3.0", - "cpufeatures", - "zeroize", -] - [[package]] name = "chacha20" version = "0.9.1" @@ -627,33 +541,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] -[[package]] -name = "chacha20poly1305" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" -dependencies = [ - "aead 0.4.3", - "chacha20 0.8.2", - "cipher 0.3.0", - "poly1305 0.7.2", - "zeroize", -] - [[package]] name = "chacha20poly1305" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead 0.5.2", - "chacha20 0.9.1", - "cipher 0.4.4", - "poly1305 0.8.0", + "aead", + "chacha20", + "cipher", + "poly1305", "zeroize", ] @@ -696,15 +597,6 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -875,6 +767,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-models" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4" +dependencies = [ + "hax-lib", + "pastey", + "rand 0.9.2", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1023,35 +926,13 @@ dependencies = [ "hybrid-array 0.4.7", ] -[[package]] -name = "ctr" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" -dependencies = [ - "cipher 0.3.0", -] - [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", + "cipher", ] [[package]] @@ -1101,19 +982,6 @@ dependencies = [ "syn", ] -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - [[package]] name = "darling" version = "0.20.11" @@ -1324,15 +1192,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" @@ -1426,15 +1285,6 @@ dependencies = [ "spki 0.7.3", ] -[[package]] -name = "ed25519" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" -dependencies = [ - "signature 1.6.4", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -1457,20 +1307,6 @@ dependencies = [ "signature 3.0.0-rc.10", ] -[[package]] -name = "ed25519-dalek" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" -dependencies = [ - "curve25519-dalek 3.2.0", - "ed25519 1.5.3", - "rand 0.7.3", - "serde", - "sha2 0.9.9", - "zeroize", -] - [[package]] name = "ed25519-dalek" version = "2.2.0" @@ -1854,17 +1690,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1874,7 +1699,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1907,16 +1732,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ghash" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" -dependencies = [ - "opaque-debug", - "polyval 0.5.3", -] - [[package]] name = "ghash" version = "0.5.1" @@ -1924,15 +1739,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval 0.6.2", + "polyval", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gloo-timers" version = "0.3.0" @@ -2091,6 +1900,43 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hax-lib" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "heapless" version = "0.7.17" @@ -2205,48 +2051,65 @@ dependencies = [ [[package]] name = "hpke-rs" -version = "0.1.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d78d066f8d487fa69d5c3f92f98c11e2540796d213016d107fe86eabf9f26b" +checksum = "7fcd4b22e7fc3318a1674085f943a35794023ecfe8b24a1691d1d1e016f869c8" dependencies = [ "hpke-rs-crypto", + "hpke-rs-libcrux", + "hpke-rs-rust-crypto", + "libcrux-sha3", "log", + "rand_core 0.9.5", "serde", - "serde_json", - "tls_codec 0.4.2", + "tls_codec", "zeroize", ] [[package]] name = "hpke-rs-crypto" -version = "0.1.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79df748353d9cee46d565f591d0039973a6554f8ef026b2647ab1ef2b64b91df" +checksum = "2dd92b7d7f0deaae59c152e01c01f5280ea92dfac82090e5c025879b32df9193" dependencies = [ - "getrandom 0.2.17", - "rand 0.8.5", - "serde", - "serde_json", - "tls_codec 0.4.2", + "rand_core 0.9.5", +] + +[[package]] +name = "hpke-rs-libcrux" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd99129e6e5ab959fca63fe83aebbd1b5ff1107eeb549dca597b6d9484e51684" +dependencies = [ + "hpke-rs-crypto", + "libcrux-aead", + "libcrux-ecdh", + "libcrux-hkdf", + "libcrux-kem", + "libcrux-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] name = "hpke-rs-rust-crypto" -version = "0.1.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d6fcfe6949aedbacad5aedb2f8ef9f054a142510e8f4f7355a4ccb3f5bd01f" +checksum = "019f9a15c71981dffb32882487c372d3e6e48557c1c1ac84f235cbded330a2ef" dependencies = [ - "aes-gcm 0.10.3", - "chacha20poly1305 0.10.1", - "getrandom 0.2.17", + "aes-gcm", + "chacha20poly1305", "hkdf", "hpke-rs-crypto", + "k256", "p256", "p384", "rand 0.8.5", "rand_chacha 0.3.1", + "rand_core 0.6.4", "sha2 0.10.9", - "x25519-dalek-ng", + "x25519-dalek", ] [[package]] @@ -2922,6 +2785,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve", +] + [[package]] name = "keccak" version = "0.1.6" @@ -2959,6 +2832,222 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libcrux-aead" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ca5c9cb6a0f4dcf2bab1b85aa302537f40b801fc5efe10b5b76fbd677e8161" +dependencies = [ + "libcrux-aesgcm", + "libcrux-chacha20poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-aesgcm" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95d897badc420310155f90ed1ea48872809c3446c94ebb116e8a810b66651623" +dependencies = [ + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6070c5d3991e208511daaf0efae2c747b14a8c136718a3a0a474a82cc0c45522" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-curve25519" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552571ff92bcdf2992b61b600c74d2eaba2c42a14d478c1e9e29391c39db8761" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-ecdh" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fceb737840ec67255068f6d90e9782ae17fad2337aeb7d7203d76560966216" +dependencies = [ + "libcrux-curve25519", + "libcrux-p256", + "rand 0.9.2", +] + +[[package]] +name = "libcrux-hacl-rs" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2637dc87d158e1f1b550fd9b226443e84153fded4de69028d897b534d16d22e6" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-hkdf" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295d04515de24bb0f81e5c46d79949517b66ba6a4aaf24328764c6f999e01e36" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-hmac", + "libcrux-secrets", +] + +[[package]] +name = "libcrux-hmac" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d081af93c27d7cebc9a8cc4b3720cba5411186297f9adeddf853d994bba4e7b" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", +] + +[[package]] +name = "libcrux-intrinsics" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa4779454e853d1de200cd12f19a8185aac47d99a5ec404cea3295c943d48f1" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-kem" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34adb7fdaddd04136e7b4b7368e680f0bca8f1392dfafbb7cb809148c6eb48c7" +dependencies = [ + "libcrux-curve25519", + "libcrux-ecdh", + "libcrux-ml-kem", + "libcrux-p256", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a930ff130a63e9d89648d0e22203ca034995191cbfa606b9f3c151ba67306963" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-p256" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a3d3d7567b86434b34a98faf19ce5a4dd20f964e0d9a2d13f02792b4ad0109" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-poly1305" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfb6399682b2dee13b728c779ab5dcc51afbe982b63508ca524806994336134" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha2" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9b200262e529493e459609895f3a02434eadb58897352236ebde491b5d6d87" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-traits", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3dabce2795479bd7294f853f7966a678cadf7a26d3d29f61cf15f5123e7ba4f" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695ff2fb97627e4d57315a2fdfbfe50df1c80c6ef7d91ba34216169bd6f41c00" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", +] + [[package]] name = "libloading" version = "0.8.9" @@ -3159,15 +3248,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "mio" version = "1.1.1" @@ -3176,7 +3256,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] @@ -3569,15 +3649,6 @@ dependencies = [ "objc2-security", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -3641,63 +3712,66 @@ dependencies = [ [[package]] name = "openmls" -version = "0.5.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95d4a03899704c17e7b280676a3301216faccf9b19eb860968f84a492aef944b" +checksum = "dcb512bfe6a55777518853ea535c6241f069cb0e8984678c117151d2a1e7e903" dependencies = [ - "backtrace", "log", "openmls_traits", "rayon", "serde", - "thiserror 1.0.69", - "tls_codec 0.3.0", + "serde_bytes", + "thiserror 2.0.18", + "tls_codec", + "zeroize", ] [[package]] -name = "openmls_memory_keystore" -version = "0.2.0" +name = "openmls_memory_storage" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1532fed34a1d3cf29962c1c07624f628501537eafac47913a08caea4bf08319e" +checksum = "1a52c927ddb9940acb96d51aebd54b8b9c601c7119e6609622fb3f2cbe16abe3" dependencies = [ + "log", "openmls_traits", + "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] name = "openmls_rust_crypto" -version = "0.2.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34960cce81b5a8b8a178f330e28895f092b49a28923d36c03fa56dd3d6f7173" +checksum = "b8bc087eeb4cf230327b156014df77464f62ee5fcd682c3292be11fbba8e7811" dependencies = [ - "aes-gcm 0.9.2", - "chacha20poly1305 0.9.1", - "ed25519-dalek 1.0.1", + "aes-gcm", + "chacha20poly1305", + "ed25519-dalek 2.2.0", "hkdf", "hmac", "hpke-rs", "hpke-rs-crypto", "hpke-rs-rust-crypto", - "openmls_memory_keystore", + "openmls_memory_storage", "openmls_traits", "p256", - "rand 0.7.3", "rand 0.8.5", "rand_chacha 0.3.1", + "serde", "sha2 0.10.9", - "thiserror 1.0.69", - "tls_codec 0.3.0", + "thiserror 2.0.18", + "tls_codec", ] [[package]] name = "openmls_traits" -version = "0.2.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a839131d13dfa50e6eef6f11f7718195c3194ac9efd81ebf2cb72e554c4e12f2" +checksum = "4f88ccdd53448dfdbfa5b8da8ba4e527c418fdb966418172bace2e3b41eedd56" dependencies = [ "serde", - "tls_codec 0.3.0", + "tls_codec", ] [[package]] @@ -3724,10 +3798,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.9", ] [[package]] @@ -3786,6 +3858,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "pem" version = "3.0.6" @@ -3986,17 +4064,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "poly1305" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash 0.4.0", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -4005,19 +4072,7 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures", "opaque-debug", - "universal-hash 0.5.1", -] - -[[package]] -name = "polyval" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash 0.4.0", + "universal-hash", ] [[package]] @@ -4029,7 +4084,7 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash 0.5.1", + "universal-hash", ] [[package]] @@ -4181,6 +4236,28 @@ dependencies = [ "toml_edit 0.23.10+spec-1.0.0", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4261,7 +4338,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -4285,7 +4362,7 @@ dependencies = [ "bincode", "capnp", "capnp-rpc", - "chacha20poly1305 0.10.1", + "chacha20poly1305", "ciborium", "clap", "crossterm", @@ -4318,6 +4395,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-util", + "toml", "tracing", "tracing-subscriber", "zeroize", @@ -4330,7 +4408,7 @@ dependencies = [ "argon2", "bincode", "capnp", - "chacha20poly1305 0.10.1", + "chacha20poly1305", "ciborium", "criterion", "ed25519-dalek 2.2.0", @@ -4340,6 +4418,7 @@ dependencies = [ "ml-kem", "opaque-ke", "openmls", + "openmls_memory_storage", "openmls_rust_crypto", "openmls_traits", "prost", @@ -4349,7 +4428,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "thiserror 1.0.69", - "tls_codec 0.3.0", + "tls_codec", "tokio", "x25519-dalek", "zeroize", @@ -4370,7 +4449,7 @@ name = "quicprochat-p2p" version = "0.1.0" dependencies = [ "anyhow", - "chacha20poly1305 0.10.1", + "chacha20poly1305", "hex", "iroh", "quicprochat-core", @@ -4430,7 +4509,7 @@ dependencies = [ "argon2", "bincode", "bytes", - "chacha20poly1305 0.10.1", + "chacha20poly1305", "futures", "hex", "opaque-ke", @@ -4587,19 +4666,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -4621,16 +4687,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -4651,15 +4707,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -4678,15 +4725,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "ratatui" version = "0.29.0" @@ -4918,12 +4956,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5295,19 +5327,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -5386,12 +5405,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - [[package]] name = "signature" version = "2.2.0" @@ -5586,12 +5599,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - [[package]] name = "syn" version = "2.0.117" @@ -5766,17 +5773,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tls_codec" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee1e621cbf57f36f5b51ebf366b57ba153be7fed133182a9513e443ecdf506e" -dependencies = [ - "serde", - "tls_codec_derive 0.3.0", - "zeroize", -] - [[package]] name = "tls_codec" version = "0.4.2" @@ -5784,21 +5780,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" dependencies = [ "serde", - "tls_codec_derive 0.4.2", + "tls_codec_derive", "zeroize", ] -[[package]] -name = "tls_codec_derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3226440488120aabe7e7cc80292634a68e541c407d97b66eceaae787454dae25" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tls_codec_derive" version = "0.4.2" @@ -6158,16 +6143,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "universal-hash" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "universal-hash" version = "0.5.1" @@ -6345,12 +6320,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -7122,18 +7091,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x25519-dalek-ng" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7074de8999662970c3c4c8f7f30925028dd8f4ca31ad4c055efa9cdf2ec326" -dependencies = [ - "curve25519-dalek-ng", - "rand 0.8.5", - "rand_core 0.6.4", - "zeroize", -] - [[package]] name = "x509-parser" version = "0.16.0" diff --git a/Cargo.toml b/Cargo.toml index 1b3a93b..2df47c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,13 @@ categories = ["cryptography", "network-programming"] [workspace.dependencies] # ── Crypto ──────────────────────────────────────────────────────────────────── -openmls = { version = "0.5", default-features = false, features = ["crypto-subtle"] } -openmls_rust_crypto = { version = "0.2" } -openmls_traits = { version = "0.2" } -# tls_codec must match the version used by openmls 0.5 (which uses 0.3) to avoid +openmls = { version = "0.8" } +openmls_rust_crypto = { version = "0.5" } +openmls_traits = { version = "0.5" } +openmls_memory_storage = { version = "0.5" } +# tls_codec must match the version used by openmls 0.8 (which uses 0.4) to avoid # duplicate Serialize trait versions in the dependency graph. -tls_codec = { version = "0.3", features = ["derive"] } +tls_codec = { version = "0.4", features = ["derive"] } # ml-kem 0.2 is the current stable release (FIPS 203, ML-KEM-768). ml-kem = { version = "0.2" } x25519-dalek = { version = "2", features = ["static_secrets"] } @@ -87,7 +88,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = { version = "1" } thiserror = { version = "1" } -# ── CLI ─────────────────────────────────────────────────────────────────────── +# ── Config / CLI ────────────────────────────────────────────────────────────── +toml = { version = "0.8" } clap = { version = "4", features = ["derive", "env"] } rustyline = { version = "14" } diff --git a/README.md b/README.md index 43392b1..d58bd85 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@

+ Why quicprochat? · Roadmap · SDK Docs · Operations · diff --git a/crates/quicprochat-client/Cargo.toml b/crates/quicprochat-client/Cargo.toml index 0104cc3..4c3d1cd 100644 --- a/crates/quicprochat-client/Cargo.toml +++ b/crates/quicprochat-client/Cargo.toml @@ -50,8 +50,9 @@ rustls = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -# CLI +# CLI + config clap = { workspace = true } +toml = { workspace = true } # Local message/conversation storage rusqlite = { workspace = true } diff --git a/crates/quicprochat-client/src/client/repl.rs b/crates/quicprochat-client/src/client/repl.rs index 5e737ad..c724290 100644 --- a/crates/quicprochat-client/src/client/repl.rs +++ b/crates/quicprochat-client/src/client/repl.rs @@ -1449,10 +1449,8 @@ pub(crate) async fn cmd_dm( }, display_name: format!("@{username}"), mls_group_blob: member - .group_ref() - .map(bincode::serialize) - .transpose() - .context("serialize group")?, + .serialize_mls_state() + .context("serialize MLS state")?, keystore_blob: None, member_keys, unread_count: 0, @@ -1493,10 +1491,8 @@ pub(crate) fn cmd_create_group(session: &mut SessionState, name: &str) -> anyhow kind: ConversationKind::Group { name: name.to_string() }, display_name: format!("#{name}"), mls_group_blob: member - .group_ref() - .map(bincode::serialize) - .transpose() - .context("serialize group")?, + .serialize_mls_state() + .context("serialize MLS state")?, keystore_blob: None, member_keys, unread_count: 0, @@ -1780,9 +1776,7 @@ pub(crate) async fn cmd_join( kind: ConversationKind::Group { name: display.clone() }, display_name: format!("#{display}"), mls_group_blob: new_member - .group_ref() - .map(bincode::serialize) - .transpose() + .serialize_mls_state() .context("serialize joined group")?, keystore_blob: None, member_keys, @@ -3186,8 +3180,9 @@ async fn try_auto_join( }; let mls_blob = member - .group_ref() - .and_then(|g| bincode::serialize(g).ok()); + .serialize_mls_state() + .ok() + .flatten(); let conv = Conversation { id: conv_id.clone(), diff --git a/crates/quicprochat-client/src/client/session.rs b/crates/quicprochat-client/src/client/session.rs index 7535160..dc6f3d9 100644 --- a/crates/quicprochat-client/src/client/session.rs +++ b/crates/quicprochat-client/src/client/session.rs @@ -16,7 +16,7 @@ use quicprochat_core::{DiskKeyStore, GroupMember, HybridKeypair, IdentityKeypair use super::conversation::{ now_ms, Conversation, ConversationId, ConversationKind, ConversationStore, }; -use super::state::{load_or_init_state, keystore_path}; +use super::state::load_or_init_state; /// Runtime state for an interactive REPL session. pub struct SessionState { @@ -109,7 +109,7 @@ impl SessionState { /// Migrate the legacy single-group from StoredState into the conversation DB. fn migrate_legacy_group( &mut self, - state_path: &Path, + _state_path: &Path, group_blob: &Option>, ) -> anyhow::Result<()> { let blob = match group_blob { @@ -117,16 +117,22 @@ impl SessionState { None => return Ok(()), }; - // Reconstruct GroupMember using the legacy keystore and group blob. - let ks_path = keystore_path(state_path); - let ks = DiskKeyStore::persistent(&ks_path)?; - let group = bincode::deserialize(blob).context("decode legacy group")?; - let member = GroupMember::new_with_state( + // Legacy group blobs used openmls 0.5 serde format. After the 0.8 + // upgrade the blob format changed to storage-provider state. Attempt + // to load from the new format; if that fails, skip the legacy group. + let group_id_guess = &blob[..blob.len().min(16)]; + let member = match GroupMember::new_from_storage_bytes( Arc::clone(&self.identity), - ks, - Some(group), + blob, + group_id_guess, false, // legacy groups are classical - ); + ) { + Ok(m) => m, + Err(e) => { + tracing::warn!(error = %e, "skipping incompatible legacy group blob (openmls version mismatch)"); + return Ok(()); + } + }; let group_id_bytes = member.group_id().unwrap_or_default(); @@ -182,26 +188,31 @@ impl SessionState { /// Create a GroupMember from a stored conversation. fn create_member_from_conv(&self, conv: &Conversation) -> anyhow::Result { - let ks_path = self.keystore_path_for(&conv.id); - let ks = DiskKeyStore::persistent(&ks_path) - .unwrap_or_else(|e| { - tracing::warn!(path = %ks_path.display(), error = %e, "DiskKeyStore open failed, falling back to ephemeral"); - DiskKeyStore::ephemeral() - }); - - let group = conv - .mls_group_blob - .as_ref() - .map(|b| bincode::deserialize(b)) - .transpose() - .context("decode MLS group from conversation db")?; - - Ok(GroupMember::new_with_state( - Arc::clone(&self.identity), - ks, - group, - conv.is_hybrid, - )) + if let Some(blob) = conv.mls_group_blob.as_ref() { + let group_id = conv.id.0.as_slice(); + let member = GroupMember::new_from_storage_bytes( + Arc::clone(&self.identity), + blob, + group_id, + conv.is_hybrid, + ) + .context("restore MLS state from conversation db")?; + Ok(member) + } else { + // No MLS state — create an empty member. + let ks_path = self.keystore_path_for(&conv.id); + let ks = DiskKeyStore::persistent(&ks_path) + .unwrap_or_else(|e| { + tracing::warn!(path = %ks_path.display(), error = %e, "DiskKeyStore open failed, falling back to ephemeral"); + DiskKeyStore::ephemeral() + }); + Ok(GroupMember::new_with_state( + Arc::clone(&self.identity), + ks, + None, + conv.is_hybrid, + )) + } } /// Path for a per-conversation keystore file. @@ -214,10 +225,8 @@ impl SessionState { pub fn save_member(&self, conv_id: &ConversationId) -> anyhow::Result<()> { let member = self.members.get(conv_id).context("no such conversation")?; let blob = member - .group_ref() - .map(bincode::serialize) - .transpose() - .context("serialize MLS group")?; + .serialize_mls_state() + .context("serialize MLS state")?; let member_keys = member.member_identities(); diff --git a/crates/quicprochat-client/src/client/state.rs b/crates/quicprochat-client/src/client/state.rs index 07db4bb..dfbd9e3 100644 --- a/crates/quicprochat-client/src/client/state.rs +++ b/crates/quicprochat-client/src/client/state.rs @@ -27,18 +27,31 @@ pub struct StoredState { /// Cached member public keys for group participants. #[serde(default)] pub member_keys: Vec>, + /// MLS group ID bytes, needed to reload the group from StorageProvider state. + #[serde(default)] + pub group_id: Option>, } impl StoredState { pub fn into_parts(self, state_path: &Path) -> anyhow::Result<(GroupMember, Option)> { let identity = Arc::new(IdentityKeypair::from_seed(self.identity_seed)); - let group = self - .group - .map(|bytes| bincode::deserialize(&bytes).context("decode group")) - .transpose()?; - let key_store = DiskKeyStore::persistent(keystore_path(state_path))?; let hybrid = self.hybrid_key.is_some(); - let member = GroupMember::new_with_state(identity, key_store, group, hybrid); + + let member = match (self.group.as_ref(), self.group_id.as_ref()) { + (Some(storage_bytes), Some(gid)) => { + GroupMember::new_from_storage_bytes( + identity, + storage_bytes, + gid, + hybrid, + ) + .context("restore MLS state from stored state")? + } + _ => { + let key_store = DiskKeyStore::persistent(keystore_path(state_path))?; + GroupMember::new_with_state(identity, key_store, None, hybrid) + } + }; let hybrid_kp = self .hybrid_key @@ -50,15 +63,15 @@ impl StoredState { pub fn from_parts(member: &GroupMember, hybrid_kp: Option<&HybridKeypair>) -> anyhow::Result { let group = member - .group_ref() - .map(|g| bincode::serialize(g).context("serialize group")) - .transpose()?; + .serialize_mls_state() + .context("serialize MLS state")?; Ok(Self { identity_seed: *member.identity_seed(), group, hybrid_key: hybrid_kp.map(|kp| kp.to_bytes()), member_keys: Vec::new(), + group_id: member.group_id(), }) } } @@ -245,6 +258,7 @@ mod tests { hybrid_key: None, group: None, member_keys: Vec::new(), + group_id: None, }; let password = "test-password"; let plaintext = bincode::serialize(&state).unwrap(); @@ -268,6 +282,7 @@ mod tests { }), group: None, member_keys: Vec::new(), + group_id: None, }; let password = "another-password"; let plaintext = bincode::serialize(&state).unwrap(); @@ -285,6 +300,7 @@ mod tests { hybrid_key: None, group: None, member_keys: Vec::new(), + group_id: None, }; let plaintext = bincode::serialize(&state).unwrap(); let encrypted = encrypt_state("correct", &plaintext).unwrap(); diff --git a/crates/quicprochat-client/src/main.rs b/crates/quicprochat-client/src/main.rs index 9b346e6..c2a4b73 100644 --- a/crates/quicprochat-client/src/main.rs +++ b/crates/quicprochat-client/src/main.rs @@ -28,12 +28,159 @@ use quicprochat_client::{ #[cfg(all(feature = "tui", not(feature = "v2")))] use quicprochat_client::client::tui::run_tui; +// ── Config file loading ────────────────────────────────────────────────────── +// +// Loads a TOML config file and sets QPQ_* environment variables for values +// not already set. This runs BEFORE clap parses, so the natural precedence is: +// CLI flags > environment variables > config file > compiled defaults. +// +// Config file search order: +// 1. --config (parsed manually from argv) +// 2. $QPC_CONFIG env var +// 3. $XDG_CONFIG_HOME/qpc/config.toml (usually ~/.config/qpc/config.toml) +// 4. ~/.qpc.toml +#[cfg(not(feature = "v2"))] +mod client_config { + use serde::Deserialize; + use std::path::PathBuf; + + #[derive(Debug, Default, Deserialize)] + pub struct ClientFileConfig { + pub server: Option, + pub server_name: Option, + pub ca_cert: Option, + pub username: Option, + pub password: Option, + pub access_token: Option, + pub device_id: Option, + pub state_password: Option, + pub state: Option, + pub danger_accept_invalid_certs: Option, + pub no_server: Option, + } + + /// Find and load the config file. Returns the parsed config (or default if + /// no file is found). + pub fn load_client_config() -> ClientFileConfig { + let path = find_config_path(); + let path = match path { + Some(p) if p.exists() => p, + _ => return ClientFileConfig::default(), + }; + + match std::fs::read_to_string(&path) { + Ok(contents) => match toml::from_str(&contents) { + Ok(cfg) => { + eprintln!("Loaded config: {}", path.display()); + cfg + } + Err(e) => { + eprintln!("Warning: failed to parse {}: {e}", path.display()); + ClientFileConfig::default() + } + }, + Err(e) => { + eprintln!("Warning: failed to read {}: {e}", path.display()); + ClientFileConfig::default() + } + } + } + + fn find_config_path() -> Option { + // 1. --config from argv (before clap parses). + let args: Vec = std::env::args().collect(); + for i in 0..args.len().saturating_sub(1) { + if args[i] == "--config" || args[i] == "-c" { + return Some(PathBuf::from(&args[i + 1])); + } + } + + // 2. $QPC_CONFIG env var. + if let Ok(p) = std::env::var("QPC_CONFIG") { + return Some(PathBuf::from(p)); + } + + // 3. $XDG_CONFIG_HOME/qpc/config.toml + let xdg = std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + PathBuf::from(home).join(".config") + }); + let xdg_path = xdg.join("qpc").join("config.toml"); + if xdg_path.exists() { + return Some(xdg_path); + } + + // 4. ~/.qpc.toml + if let Ok(home) = std::env::var("HOME") { + let home_path = PathBuf::from(home).join(".qpc.toml"); + if home_path.exists() { + return Some(home_path); + } + } + + None + } + + /// Set QPQ_* env vars from config values, but only if they're not already set. + pub fn apply_config_to_env(cfg: &ClientFileConfig) { + fn set_if_empty(key: &str, val: &str) { + if std::env::var(key).is_err() { + std::env::set_var(key, val); + } + } + + if let Some(ref v) = cfg.server { + set_if_empty("QPQ_SERVER", v); + } + if let Some(ref v) = cfg.server_name { + set_if_empty("QPQ_SERVER_NAME", v); + } + if let Some(ref v) = cfg.ca_cert { + set_if_empty("QPQ_CA_CERT", v); + } + if let Some(ref v) = cfg.username { + set_if_empty("QPQ_USERNAME", v); + } + if let Some(ref v) = cfg.password { + set_if_empty("QPQ_PASSWORD", v); + } + if let Some(ref v) = cfg.access_token { + set_if_empty("QPQ_ACCESS_TOKEN", v); + } + if let Some(ref v) = cfg.device_id { + set_if_empty("QPQ_DEVICE_ID", v); + } + if let Some(ref v) = cfg.state_password { + set_if_empty("QPQ_STATE_PASSWORD", v); + } + if let Some(ref v) = cfg.state { + set_if_empty("QPQ_STATE", v); + } + if let Some(v) = cfg.danger_accept_invalid_certs { + if v { + set_if_empty("QPQ_DANGER_ACCEPT_INVALID_CERTS", "true"); + } + } + if let Some(v) = cfg.no_server { + if v { + set_if_empty("QPQ_NO_SERVER", "true"); + } + } + } +} + // ── CLI ─────────────────────────────────────────────────────────────────────── #[cfg(not(feature = "v2"))] #[derive(Debug, Parser)] #[command(name = "qpc", about = "quicprochat CLI client", version)] struct Args { + /// Path to a TOML config file (auto-detected from ~/.config/qpc/config.toml or ~/.qpc.toml). + #[arg(long, short = 'c', global = true, env = "QPC_CONFIG")] + config: Option, + /// Path to the server's TLS certificate (self-signed by default). #[arg( long, @@ -540,6 +687,13 @@ async fn main() -> anyhow::Result<()> { ) .init(); + // Load config file and apply to env BEFORE clap parses (so config values + // act as defaults that env vars and CLI flags can override). + { + let cfg = client_config::load_client_config(); + client_config::apply_config_to_env(&cfg); + } + let args = Args::parse(); if args.danger_accept_invalid_certs { diff --git a/crates/quicprochat-core/Cargo.toml b/crates/quicprochat-core/Cargo.toml index d535183..608026e 100644 --- a/crates/quicprochat-core/Cargo.toml +++ b/crates/quicprochat-core/Cargo.toml @@ -15,6 +15,7 @@ native = [ "dep:openmls", "dep:openmls_rust_crypto", "dep:openmls_traits", + "dep:openmls_memory_storage", "dep:tls_codec", "dep:opaque-ke", "dep:bincode", @@ -49,6 +50,7 @@ opaque-ke = { workspace = true, optional = true } openmls = { workspace = true, optional = true } openmls_rust_crypto = { workspace = true, optional = true } openmls_traits = { workspace = true, optional = true } +openmls_memory_storage = { workspace = true, optional = true } tls_codec = { workspace = true, optional = true } bincode = { workspace = true, optional = true } diff --git a/crates/quicprochat-core/src/group.rs b/crates/quicprochat-core/src/group.rs index e420aa4..c7c0d19 100644 --- a/crates/quicprochat-core/src/group.rs +++ b/crates/quicprochat-core/src/group.rs @@ -29,7 +29,7 @@ //! # Ratchet tree //! //! `use_ratchet_tree_extension = true` so that the ratchet tree is embedded -//! in Welcome messages. `new_from_welcome` is called with `ratchet_tree = None`; +//! in Welcome messages. `new_from_welcome` is called without a ratchet_tree; //! openmls extracts the tree from the Welcome's `GroupInfo` extension. use std::{path::Path, sync::Arc}; @@ -37,12 +37,13 @@ use std::{path::Path, sync::Arc}; use zeroize::Zeroizing; use openmls::prelude::{ - Ciphersuite, Credential, CredentialType, CredentialWithKey, CryptoConfig, GroupId, KeyPackage, - KeyPackageIn, MlsGroup, MlsGroupConfig, MlsMessageInBody, MlsMessageOut, - ProcessedMessageContent, ProtocolMessage, ProtocolVersion, TlsDeserializeTrait, - TlsSerializeTrait, + BasicCredential, Ciphersuite, Credential, CredentialWithKey, GroupId, KeyPackage, + KeyPackageIn, LeafNodeParameters, MlsGroup, MlsGroupCreateConfig, MlsGroupJoinConfig, + MlsMessageBodyIn, MlsMessageOut, ProcessedMessageContent, ProtocolMessage, + ProtocolVersion, StagedWelcome, }; -use openmls_traits::OpenMlsCryptoProvider; +use openmls_traits::OpenMlsProvider; +use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait}; use crate::{ error::CoreError, @@ -102,8 +103,10 @@ pub struct GroupMember { identity: Arc, /// Active MLS group, if any. group: Option, - /// Shared group configuration (wire format, ratchet tree extension, etc.). - config: MlsGroupConfig, + /// Shared group creation configuration (wire format, ratchet tree extension, etc.). + create_config: MlsGroupCreateConfig, + /// Shared group join configuration (wire format, ratchet tree extension, etc.). + join_config: MlsGroupJoinConfig, /// Whether this member uses hybrid (X25519 + ML-KEM-768) HPKE keys. hybrid: bool, } @@ -139,7 +142,11 @@ impl GroupMember { group: Option, hybrid: bool, ) -> Self { - let config = MlsGroupConfig::builder() + let create_config = MlsGroupCreateConfig::builder() + .use_ratchet_tree_extension(true) + .build(); + + let join_config = MlsGroupJoinConfig::builder() .use_ratchet_tree_extension(true) .build(); @@ -153,7 +160,8 @@ impl GroupMember { backend, identity, group, - config, + create_config, + join_config, hybrid, } } @@ -175,18 +183,19 @@ impl GroupMember { /// /// Returns [`CoreError::Mls`] if openmls fails to create the KeyPackage. pub fn generate_key_package(&mut self) -> Result, CoreError> { - let credential_with_key = self.make_credential_with_key()?; + let credential_with_key = self.make_credential_with_key(); - let key_package = KeyPackage::builder() + let key_package_bundle = KeyPackage::builder() .build( - CryptoConfig::with_default_version(CIPHERSUITE), + CIPHERSUITE, &self.backend, self.identity.as_ref(), credential_with_key, ) .map_err(|e| CoreError::Mls(format!("{e:?}")))?; - key_package + key_package_bundle + .key_package() .tls_serialize_detached() .map_err(|e| CoreError::Mls(format!("{e:?}"))) } @@ -205,13 +214,13 @@ impl GroupMember { /// /// Returns [`CoreError::Mls`] if the group already exists or openmls fails. pub fn create_group(&mut self, group_id: &[u8]) -> Result<(), CoreError> { - let credential_with_key = self.make_credential_with_key()?; + let credential_with_key = self.make_credential_with_key(); let mls_id = GroupId::from_slice(group_id); let group = MlsGroup::new_with_group_id( &self.backend, self.identity.as_ref(), - &self.config, + &self.create_config, mls_id, credential_with_key, ) @@ -303,7 +312,7 @@ impl GroupMember { let leaf_index = group .members() - .find(|m| m.credential.identity() == member_identity) + .find(|m| m.credential.serialized_content() == member_identity) .map(|m| m.index) .ok_or_else(|| CoreError::Mls("member not found in group".into()))?; @@ -384,7 +393,11 @@ impl GroupMember { .ok_or_else(|| CoreError::Mls("no active group".into()))?; let (proposal_out, _ref) = group - .propose_self_update(&self.backend, self.identity.as_ref(), None) + .propose_self_update( + &self.backend, + self.identity.as_ref(), + LeafNodeParameters::default(), + ) .map_err(|e| CoreError::Mls(format!("propose_self_update: {e:?}")))?; proposal_out @@ -396,7 +409,7 @@ impl GroupMember { pub fn has_pending_proposals(&self) -> bool { self.group .as_ref() - .map(|g| g.pending_proposals().next().is_some()) + .map(|g| g.has_pending_proposals()) .unwrap_or(false) } @@ -417,16 +430,22 @@ impl GroupMember { let msg_in = openmls::prelude::MlsMessageIn::tls_deserialize(&mut welcome_bytes) .map_err(|e| CoreError::Mls(format!("Welcome deserialise: {e:?}")))?; - // into_welcome() is feature-gated in openmls 0.5; extract() is public. let welcome = match msg_in.extract() { - MlsMessageInBody::Welcome(w) => w, + MlsMessageBodyIn::Welcome(w) => w, _ => return Err(CoreError::Mls("expected a Welcome message".into())), }; - // ratchet_tree = None because use_ratchet_tree_extension = true embeds - // the tree inside the Welcome's GroupInfo extension. - let group = MlsGroup::new_from_welcome(&self.backend, &self.config, welcome, None) - .map_err(|e| CoreError::Mls(format!("new_from_welcome: {e:?}")))?; + let staged = StagedWelcome::new_from_welcome( + &self.backend, + &self.join_config, + welcome, + None, // ratchet tree extracted from the Welcome's GroupInfo extension + ) + .map_err(|e| CoreError::Mls(format!("new_from_welcome: {e:?}")))?; + + let group = staged + .into_group(&self.backend) + .map_err(|e| CoreError::Mls(format!("into_group: {e:?}")))?; self.group = Some(group); Ok(()) @@ -508,10 +527,9 @@ impl GroupMember { let msg_in = openmls::prelude::MlsMessageIn::tls_deserialize(&mut bytes) .map_err(|e| CoreError::Mls(format!("message deserialise: {e:?}")))?; - // into_protocol_message() is feature-gated; extract() + manual construction is not. - let protocol_message = match msg_in.extract() { - MlsMessageInBody::PrivateMessage(m) => ProtocolMessage::PrivateMessage(m), - MlsMessageInBody::PublicMessage(m) => ProtocolMessage::PublicMessage(m), + let protocol_message: ProtocolMessage = match msg_in.extract() { + MlsMessageBodyIn::PrivateMessage(m) => m.into(), + MlsMessageBodyIn::PublicMessage(m) => m.into(), _ => return Err(CoreError::Mls("not a protocol message".into())), }; @@ -519,7 +537,7 @@ impl GroupMember { .process_message(&self.backend, protocol_message) .map_err(|e| CoreError::Mls(format!("process_message: {e:?}")))?; - let sender_identity = processed.credential().identity().to_vec(); + let sender_identity = processed.credential().serialized_content().to_vec(); match processed.into_content() { ProcessedMessageContent::ApplicationMessage(app) => { @@ -545,11 +563,15 @@ impl GroupMember { } // Proposals are stored for a later Commit; nothing to return yet. ProcessedMessageContent::ProposalMessage(proposal) => { - group.store_pending_proposal(*proposal); + group + .store_pending_proposal(self.backend.storage(), *proposal) + .map_err(|e| CoreError::Mls(format!("store_pending_proposal: {e:?}")))?; Ok((sender_identity, ReceivedMessage::StateChanged)) } ProcessedMessageContent::ExternalJoinProposalMessage(proposal) => { - group.store_pending_proposal(*proposal); + group + .store_pending_proposal(self.backend.storage(), *proposal) + .map_err(|e| CoreError::Mls(format!("store_pending_proposal: {e:?}")))?; Ok((sender_identity, ReceivedMessage::StateChanged)) } } @@ -597,6 +619,69 @@ impl GroupMember { self.group.as_ref() } + /// Serialize the MLS group state (via the backing `StorageProvider`). + /// + /// In openmls 0.8 the `MlsGroup` is no longer `Serialize`; its state is + /// held inside the `StorageProvider`. This method serializes the full + /// provider storage to bytes, which can later be restored with + /// [`new_from_storage_bytes`]. + /// + /// Returns `None` if no active group exists. + /// + /// [`new_from_storage_bytes`]: Self::new_from_storage_bytes + pub fn serialize_mls_state(&self) -> Result>, CoreError> { + if self.group.is_none() { + return Ok(None); + } + let bytes = self + .backend + .storage() + .to_bytes() + .map_err(|e| CoreError::Mls(format!("serialize storage: {e}")))?; + Ok(Some(bytes)) + } + + /// Create a `GroupMember` from previously serialized storage bytes. + /// + /// Reconstructs the `DiskKeyStore` from the blob, then loads the + /// `MlsGroup` from the storage provider using the given `group_id`. + pub fn new_from_storage_bytes( + identity: Arc, + storage_bytes: &[u8], + group_id: &[u8], + hybrid: bool, + ) -> Result { + let key_store = DiskKeyStore::from_bytes(storage_bytes) + .map_err(|e| CoreError::Mls(format!("deserialize storage: {e}")))?; + + let create_config = MlsGroupCreateConfig::builder() + .use_ratchet_tree_extension(true) + .build(); + + let join_config = MlsGroupJoinConfig::builder() + .use_ratchet_tree_extension(true) + .build(); + + let backend = if hybrid { + HybridCryptoProvider::new_hybrid(key_store) + } else { + HybridCryptoProvider::new_classical(key_store) + }; + + let mls_group_id = GroupId::from_slice(group_id); + let group = MlsGroup::load(backend.storage(), &mls_group_id) + .map_err(|e| CoreError::Mls(format!("load group from storage: {e}")))?; + + Ok(Self { + backend, + identity, + group, + create_config, + join_config, + hybrid, + }) + } + /// Return the identity (credential) bytes of all current group members. /// /// Each entry is the raw credential payload (Ed25519 public key bytes) @@ -608,23 +693,20 @@ impl GroupMember { }; group .members() - .map(|m| m.credential.identity().to_vec()) + .map(|m| m.credential.serialized_content().to_vec()) .collect() } // ── Private helpers ─────────────────────────────────────────────────────── - fn make_credential_with_key(&self) -> Result { - let credential = Credential::new( - self.identity.public_key_bytes().to_vec(), - CredentialType::Basic, - ) - .map_err(|e| CoreError::Mls(format!("{e:?}")))?; + fn make_credential_with_key(&self) -> CredentialWithKey { + let credential: Credential = + BasicCredential::new(self.identity.public_key_bytes().to_vec()).into(); - Ok(CredentialWithKey { + CredentialWithKey { credential, signature_key: self.identity.public_key_bytes().to_vec().into(), - }) + } } } @@ -758,11 +840,6 @@ mod tests { let (_commit_a, welcome_a) = creator.add_member(&a_kp).expect("add A"); a.join_group(&welcome_a).expect("A join"); - // A must process the commit that added them (it's a StateChanged for A since - // the commit itself is what brought them in — but actually A joined via Welcome, - // so A doesn't process the add-commit). The creator already merged the pending - // commit in add_member, so creator is at epoch 2. - // Add B — at this point creator is at epoch 2 (after adding A). let (commit_b, welcome_b) = creator.add_member(&b_kp).expect("add B"); b.join_group(&welcome_b).expect("B join"); @@ -958,7 +1035,7 @@ mod tests { ); } - /// 10 messages alternating Alice→Bob and Bob→Alice all decrypt successfully. + /// 10 messages alternating Alice->Bob and Bob->Alice all decrypt successfully. /// Verifies that epoch state stays in sync across multiple application messages. #[test] fn multi_message_roundtrip_epoch_stays_in_sync() { diff --git a/crates/quicprochat-core/src/hybrid_crypto.rs b/crates/quicprochat-core/src/hybrid_crypto.rs index 3547809..029a7e0 100644 --- a/crates/quicprochat-core/src/hybrid_crypto.rs +++ b/crates/quicprochat-core/src/hybrid_crypto.rs @@ -27,8 +27,9 @@ use openmls_traits::{ crypto::OpenMlsCrypto, types::{ CryptoError, ExporterSecret, HpkeCiphertext, HpkeConfig, HpkeKeyPair, HpkeKemType, + KemOutput, }, - OpenMlsCryptoProvider, + OpenMlsProvider, }; use tls_codec::SecretVLBytes; @@ -128,6 +129,15 @@ impl OpenMlsCrypto for HybridCrypto { self.rust_crypto.hkdf_extract(hash_type, salt, ikm) } + fn hmac( + &self, + hash_type: HashType, + key: &[u8], + message: &[u8], + ) -> Result { + self.rust_crypto.hmac(hash_type, key, message) + } + fn hkdf_expand( &self, hash_type: HashType, @@ -189,25 +199,18 @@ impl OpenMlsCrypto for HybridCrypto { info: &[u8], aad: &[u8], ptxt: &[u8], - ) -> HpkeCiphertext { + ) -> Result { if Self::is_hybrid_public_key(pk_r) { - // The trait `OpenMlsCrypto::hpke_seal` returns `HpkeCiphertext` (not - // `Result`), so we cannot propagate errors through the return type. - // Returning an empty ciphertext would silently cause data loss. - // Instead, panic on failure — a hybrid key that passes the length - // check but fails deserialization or encryption indicates a critical - // bug (corrupted key material), not a recoverable condition. let recipient_pk = HybridPublicKey::from_bytes(pk_r) - .expect("hybrid public key deserialization failed — key material is corrupted"); - // Pass HPKE info and aad through for proper context binding (RFC 9180). + .map_err(|_| CryptoError::CryptoLibraryError)?; let envelope = hybrid_encrypt(&recipient_pk, ptxt, info, aad) - .expect("hybrid HPKE encryption failed — critical crypto error"); + .map_err(|_| CryptoError::CryptoLibraryError)?; let kem_output = envelope[..HYBRID_KEM_OUTPUT_LEN].to_vec(); let ciphertext = envelope[HYBRID_KEM_OUTPUT_LEN..].to_vec(); - HpkeCiphertext { + Ok(HpkeCiphertext { kem_output: kem_output.into(), ciphertext: ciphertext.into(), - } + }) } else { self.rust_crypto.hpke_seal(config, pk_r, info, aad, ptxt) } @@ -245,7 +248,7 @@ impl OpenMlsCrypto for HybridCrypto { info: &[u8], exporter_context: &[u8], exporter_length: usize, - ) -> Result<(Vec, ExporterSecret), CryptoError> { + ) -> Result<(KemOutput, ExporterSecret), CryptoError> { if Self::is_hybrid_public_key(pk_r) { // A key that passes the hybrid length check but fails deserialization // is corrupted — return an error instead of silently downgrading to @@ -286,14 +289,14 @@ impl OpenMlsCrypto for HybridCrypto { } } - fn derive_hpke_keypair(&self, config: HpkeConfig, ikm: &[u8]) -> HpkeKeyPair { + fn derive_hpke_keypair(&self, config: HpkeConfig, ikm: &[u8]) -> Result { if self.hybrid_enabled && config.0 == HpkeKemType::DhKem25519 { let kp = HybridKeypair::derive_from_ikm(ikm); let private_bytes = kp.private_to_bytes(); - HpkeKeyPair { + Ok(HpkeKeyPair { private: private_bytes.as_slice().into(), public: kp.public_key().to_bytes(), - } + }) } else { self.rust_crypto.derive_hpke_keypair(config, ikm) } @@ -343,10 +346,10 @@ impl Default for HybridCryptoProvider { } } -impl OpenMlsCryptoProvider for HybridCryptoProvider { +impl OpenMlsProvider for HybridCryptoProvider { type CryptoProvider = HybridCrypto; type RandProvider = RustCrypto; - type KeyStoreProvider = DiskKeyStore; + type StorageProvider = DiskKeyStore; fn crypto(&self) -> &Self::CryptoProvider { &self.crypto @@ -356,7 +359,7 @@ impl OpenMlsCryptoProvider for HybridCryptoProvider { self.crypto.rust_crypto() } - fn key_store(&self) -> &Self::KeyStoreProvider { + fn storage(&self) -> &Self::StorageProvider { &self.key_store } } @@ -383,7 +386,7 @@ mod tests { let crypto = HybridCrypto::new(); let ikm = b"test-ikm-for-hybrid-hpke-keypair"; - let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm); + let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm).unwrap(); assert_eq!(keypair.public.len(), HYBRID_PUBLIC_KEY_LEN); assert_eq!(keypair.private.as_ref().len(), HYBRID_PRIVATE_KEY_LEN); @@ -397,7 +400,7 @@ mod tests { info, aad, plaintext, - ); + ).unwrap(); assert!(!ct.kem_output.as_slice().is_empty()); assert!(!ct.ciphertext.as_slice().is_empty()); @@ -419,7 +422,7 @@ mod tests { let crypto = HybridCrypto::new(); let ikm = b"exporter-ikm"; - let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm); + let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm).unwrap(); let info = b""; let exporter_context = b"MLS 1.0 external init"; let exporter_length = 32; @@ -457,7 +460,7 @@ mod tests { let crypto = HybridCrypto::new_classical(); let ikm = b"test-ikm-for-classical-hpke"; - let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm); + let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm).unwrap(); // Classical X25519 keys are 32 bytes assert_eq!(keypair.public.len(), 32); assert_eq!(keypair.private.as_ref().len(), 32); @@ -469,7 +472,7 @@ mod tests { let crypto = HybridCrypto::new_classical(); let ikm = b"test-ikm-for-classical-round-trip"; - let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm); + let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm).unwrap(); assert_eq!(keypair.public.len(), 32); // classical key let plaintext = b"hello classical MLS"; @@ -482,7 +485,7 @@ mod tests { info, aad, plaintext, - ); + ).unwrap(); assert!(!ct.kem_output.as_slice().is_empty()); let decrypted = crypto @@ -501,7 +504,7 @@ mod tests { #[test] fn key_package_generation_with_hybrid_provider() { use openmls::prelude::{ - Credential, CredentialType, CredentialWithKey, CryptoConfig, KeyPackage, + BasicCredential, CredentialWithKey, KeyPackage, }; use std::sync::Arc; use tls_codec::Serialize; @@ -514,26 +517,24 @@ mod tests { let provider = HybridCryptoProvider::default(); let identity = Arc::new(IdentityKeypair::generate()); - let credential = Credential::new( - identity.public_key_bytes().to_vec(), - CredentialType::Basic, - ) - .unwrap(); + let credential: openmls::prelude::Credential = + BasicCredential::new(identity.public_key_bytes().to_vec()).into(); let credential_with_key = CredentialWithKey { credential, signature_key: identity.public_key_bytes().to_vec().into(), }; - let key_package = KeyPackage::builder() + let key_package_bundle = KeyPackage::builder() .build( - CryptoConfig::with_default_version(CIPHERSUITE), + CIPHERSUITE, &provider, identity.as_ref(), credential_with_key, ) .expect("KeyPackage with hybrid HPKE"); - let bytes = key_package + let bytes = key_package_bundle + .key_package() .tls_serialize_detached() .expect("serialize KeyPackage"); assert!(!bytes.is_empty()); diff --git a/crates/quicprochat-core/src/identity.rs b/crates/quicprochat-core/src/identity.rs index f8ed524..a52f11f 100644 --- a/crates/quicprochat-core/src/identity.rs +++ b/crates/quicprochat-core/src/identity.rs @@ -90,7 +90,7 @@ impl IdentityKeypair { /// `openmls_basic_credential` crate. #[cfg(feature = "native")] impl openmls_traits::signatures::Signer for IdentityKeypair { - fn sign(&self, payload: &[u8]) -> Result, openmls_traits::types::Error> { + fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { let sk = self.signing_key(); let sig: ed25519_dalek::Signature = sk.sign(payload); Ok(sig.to_bytes().to_vec()) diff --git a/crates/quicprochat-core/src/keypackage.rs b/crates/quicprochat-core/src/keypackage.rs index cf808b0..b0a6481 100644 --- a/crates/quicprochat-core/src/keypackage.rs +++ b/crates/quicprochat-core/src/keypackage.rs @@ -17,10 +17,10 @@ //! The resulting bytes are opaque to the quicprochat transport layer. use openmls::prelude::{ - Ciphersuite, Credential, CredentialType, CredentialWithKey, CryptoConfig, KeyPackage, - KeyPackageIn, TlsDeserializeTrait, TlsSerializeTrait, + BasicCredential, Ciphersuite, CredentialWithKey, KeyPackage, KeyPackageIn, }; use openmls_rust_crypto::OpenMlsRustCrypto; +use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait}; use sha2::{Digest, Sha256}; use crate::{error::CoreError, identity::IdentityKeypair}; @@ -74,8 +74,8 @@ pub fn generate_key_package(identity: &IdentityKeypair) -> Result<(Vec, Vec< // Build a BasicCredential using the raw Ed25519 public key bytes as the // MLS identity. Per RFC 9420, any byte string may serve as the identity. - let credential = Credential::new(identity.public_key_bytes().to_vec(), CredentialType::Basic) - .map_err(|e| CoreError::Mls(format!("{e:?}")))?; + let credential: openmls::prelude::Credential = + BasicCredential::new(identity.public_key_bytes().to_vec()).into(); // The `signature_key` in CredentialWithKey is the Ed25519 public key that // will be used to verify the KeyPackage's leaf node signature. @@ -87,19 +87,13 @@ pub fn generate_key_package(identity: &IdentityKeypair) -> Result<(Vec, Vec< // `IdentityKeypair` implements `openmls_traits::signatures::Signer` // so it can be passed directly to the builder. - let key_package = KeyPackage::builder() - .build( - CryptoConfig::with_default_version(CIPHERSUITE), - &backend, - identity, - credential_with_key, - ) + let key_package_bundle = KeyPackage::builder() + .build(CIPHERSUITE, &backend, identity, credential_with_key) .map_err(|e| CoreError::Mls(format!("{e:?}")))?; - // TLS-encode the KeyPackage using the trait from the openmls prelude. - // This uses tls_codec 0.3 (the same version openmls uses internally), - // avoiding a duplicate-trait conflict with tls_codec 0.4. - let tls_bytes = key_package + // TLS-encode the KeyPackage. + let tls_bytes = key_package_bundle + .key_package() .tls_serialize_detached() .map_err(|e| CoreError::Mls(format!("{e:?}")))?; diff --git a/crates/quicprochat-core/src/keystore.rs b/crates/quicprochat-core/src/keystore.rs index aaa88b8..463d364 100644 --- a/crates/quicprochat-core/src/keystore.rs +++ b/crates/quicprochat-core/src/keystore.rs @@ -1,23 +1,21 @@ use std::{ - collections::HashMap, fs, path::{Path, PathBuf}, - sync::RwLock, }; -use openmls_traits::key_store::{MlsEntity, OpenMlsKeyStore}; +use openmls_memory_storage::MemoryStorage; +use openmls_traits::storage::{traits, StorageProvider, CURRENT_VERSION}; -/// A disk-backed key store implementing `OpenMlsKeyStore`. +/// A disk-backed storage provider implementing `StorageProvider`. /// -/// In-memory when `path` is `None`; otherwise flushes the entire map to disk on -/// every store/delete so HPKE init keys survive process restarts. +/// Wraps `openmls_memory_storage::MemoryStorage` and flushes to disk on every +/// write so that HPKE init keys and group state survive process restarts. /// /// # Serialization /// -/// Uses bincode for both individual MLS entity values and the outer HashMap -/// container. This is required because OpenMLS types use bincode-compatible -/// serialization, and `HashMap, Vec>` requires a binary format -/// (JSON mandates string keys). +/// Uses bincode for the outer `HashMap, Vec>` container when +/// persisting to disk. The inner values use serde_json (matching +/// `MemoryStorage`'s serialization format). /// /// # Persistence security /// @@ -26,15 +24,17 @@ use openmls_traits::key_store::{MlsEntity, OpenMlsKeyStore}; #[derive(Debug)] pub struct DiskKeyStore { path: Option, - values: RwLock, Vec>>, + storage: MemoryStorage, } -#[derive(thiserror::Error, Debug, PartialEq, Eq)] +#[derive(thiserror::Error, Debug)] pub enum DiskKeyStoreError { #[error("serialization error")] Serialization, #[error("io error: {0}")] Io(String), + #[error("memory storage error: {0}")] + MemoryStorage(#[from] openmls_memory_storage::MemoryStorageError), } impl DiskKeyStore { @@ -42,28 +42,35 @@ impl DiskKeyStore { pub fn ephemeral() -> Self { Self { path: None, - values: RwLock::new(HashMap::new()), + storage: MemoryStorage::default(), } } /// Persistent keystore backed by `path`. Creates an empty store if missing. pub fn persistent(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); - let values = if path.exists() { + let storage = if path.exists() { let bytes = fs::read(&path).map_err(|e| DiskKeyStoreError::Io(e.to_string()))?; if bytes.is_empty() { - HashMap::new() + MemoryStorage::default() } else { - bincode::deserialize(&bytes) - .map_err(|_| DiskKeyStoreError::Serialization)? + let map: std::collections::HashMap, Vec> = + bincode::deserialize(&bytes) + .map_err(|_| DiskKeyStoreError::Serialization)?; + let storage = MemoryStorage::default(); + let mut values = storage.values.write() + .map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; + *values = map; + drop(values); + storage } } else { - HashMap::new() + MemoryStorage::default() }; let store = Self { path: Some(path), - values: RwLock::new(values), + storage, }; // Set restrictive file permissions on the keystore file. @@ -76,8 +83,10 @@ impl DiskKeyStore { let Some(path) = &self.path else { return Ok(()); }; - let values = self.values.read().map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; - let bytes = bincode::serialize(&*values).map_err(|_| DiskKeyStoreError::Serialization)?; + let values = self.storage.values.read() + .map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; + let bytes = bincode::serialize(&*values) + .map_err(|_| DiskKeyStoreError::Serialization)?; if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|e| DiskKeyStoreError::Io(e.to_string()))?; } @@ -86,6 +95,32 @@ impl DiskKeyStore { Ok(()) } + /// Serialize the backing storage to bytes (bincode). + /// + /// This captures all key material *and* MLS group state held by the + /// `StorageProvider`, allowing the caller to persist it in a database + /// column instead of (or in addition to) on-disk files. + pub fn to_bytes(&self) -> Result, DiskKeyStoreError> { + let values = self.storage.values.read() + .map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; + bincode::serialize(&*values).map_err(|_| DiskKeyStoreError::Serialization) + } + + /// Restore a `DiskKeyStore` from bytes previously produced by [`to_bytes`]. + pub fn from_bytes(bytes: &[u8]) -> Result { + let map: std::collections::HashMap, Vec> = + bincode::deserialize(bytes).map_err(|_| DiskKeyStoreError::Serialization)?; + let storage = MemoryStorage::default(); + let mut values = storage.values.write() + .map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; + *values = map; + drop(values); + Ok(Self { + path: None, + storage, + }) + } + /// Restrict file permissions to owner-only (0o600) on Unix. #[cfg(unix)] fn set_file_permissions(&self) -> Result<(), DiskKeyStoreError> { @@ -112,31 +147,567 @@ impl Default for DiskKeyStore { } } -impl OpenMlsKeyStore for DiskKeyStore { +/// Delegate all `StorageProvider` methods to the inner `MemoryStorage`, +/// flushing to disk after every write/delete operation. +/// +/// The flush errors are mapped to `DiskKeyStoreError` via the +/// `MemoryStorageError` conversion. If a flush fails, the in-memory state +/// is still updated (matching the old DiskKeyStore behavior). +impl StorageProvider for DiskKeyStore { type Error = DiskKeyStoreError; - fn store(&self, k: &[u8], v: &V) -> Result<(), Self::Error> { - let value = bincode::serialize(v).map_err(|_| DiskKeyStoreError::Serialization)?; - let mut values = self.values.write().map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; - values.insert(k.to_vec(), value); - drop(values); + fn write_mls_join_config< + GroupId: traits::GroupId, + MlsGroupJoinConfig: traits::MlsGroupJoinConfig, + >( + &self, + group_id: &GroupId, + config: &MlsGroupJoinConfig, + ) -> Result<(), Self::Error> { + self.storage.write_mls_join_config(group_id, config)?; self.flush() } - fn read(&self, k: &[u8]) -> Option { - let values = match self.values.read() { - Ok(v) => v, - Err(_) => return None, - }; - values - .get(k) - .and_then(|bytes| bincode::deserialize(bytes).ok()) + fn append_own_leaf_node< + GroupId: traits::GroupId, + LeafNode: traits::LeafNode, + >( + &self, + group_id: &GroupId, + leaf_node: &LeafNode, + ) -> Result<(), Self::Error> { + self.storage.append_own_leaf_node(group_id, leaf_node)?; + self.flush() } - fn delete(&self, k: &[u8]) -> Result<(), Self::Error> { - let mut values = self.values.write().map_err(|_| DiskKeyStoreError::Io("lock poisoned".into()))?; - values.remove(k); - drop(values); + fn queue_proposal< + GroupId: traits::GroupId, + ProposalRef: traits::ProposalRef, + QueuedProposal: traits::QueuedProposal, + >( + &self, + group_id: &GroupId, + proposal_ref: &ProposalRef, + proposal: &QueuedProposal, + ) -> Result<(), Self::Error> { + self.storage.queue_proposal(group_id, proposal_ref, proposal)?; + self.flush() + } + + fn write_tree< + GroupId: traits::GroupId, + TreeSync: traits::TreeSync, + >( + &self, + group_id: &GroupId, + tree: &TreeSync, + ) -> Result<(), Self::Error> { + self.storage.write_tree(group_id, tree)?; + self.flush() + } + + fn write_interim_transcript_hash< + GroupId: traits::GroupId, + InterimTranscriptHash: traits::InterimTranscriptHash, + >( + &self, + group_id: &GroupId, + interim_transcript_hash: &InterimTranscriptHash, + ) -> Result<(), Self::Error> { + self.storage.write_interim_transcript_hash(group_id, interim_transcript_hash)?; + self.flush() + } + + fn write_context< + GroupId: traits::GroupId, + GroupContext: traits::GroupContext, + >( + &self, + group_id: &GroupId, + group_context: &GroupContext, + ) -> Result<(), Self::Error> { + self.storage.write_context(group_id, group_context)?; + self.flush() + } + + fn write_confirmation_tag< + GroupId: traits::GroupId, + ConfirmationTag: traits::ConfirmationTag, + >( + &self, + group_id: &GroupId, + confirmation_tag: &ConfirmationTag, + ) -> Result<(), Self::Error> { + self.storage.write_confirmation_tag(group_id, confirmation_tag)?; + self.flush() + } + + fn write_group_state< + GroupState: traits::GroupState, + GroupId: traits::GroupId, + >( + &self, + group_id: &GroupId, + group_state: &GroupState, + ) -> Result<(), Self::Error> { + self.storage.write_group_state(group_id, group_state)?; + self.flush() + } + + fn write_message_secrets< + GroupId: traits::GroupId, + MessageSecrets: traits::MessageSecrets, + >( + &self, + group_id: &GroupId, + message_secrets: &MessageSecrets, + ) -> Result<(), Self::Error> { + self.storage.write_message_secrets(group_id, message_secrets)?; + self.flush() + } + + fn write_resumption_psk_store< + GroupId: traits::GroupId, + ResumptionPskStore: traits::ResumptionPskStore, + >( + &self, + group_id: &GroupId, + resumption_psk_store: &ResumptionPskStore, + ) -> Result<(), Self::Error> { + self.storage.write_resumption_psk_store(group_id, resumption_psk_store)?; + self.flush() + } + + fn write_own_leaf_index< + GroupId: traits::GroupId, + LeafNodeIndex: traits::LeafNodeIndex, + >( + &self, + group_id: &GroupId, + own_leaf_index: &LeafNodeIndex, + ) -> Result<(), Self::Error> { + self.storage.write_own_leaf_index(group_id, own_leaf_index)?; + self.flush() + } + + fn write_group_epoch_secrets< + GroupId: traits::GroupId, + GroupEpochSecrets: traits::GroupEpochSecrets, + >( + &self, + group_id: &GroupId, + group_epoch_secrets: &GroupEpochSecrets, + ) -> Result<(), Self::Error> { + self.storage.write_group_epoch_secrets(group_id, group_epoch_secrets)?; + self.flush() + } + + fn write_signature_key_pair< + SignaturePublicKey: traits::SignaturePublicKey, + SignatureKeyPair: traits::SignatureKeyPair, + >( + &self, + public_key: &SignaturePublicKey, + signature_key_pair: &SignatureKeyPair, + ) -> Result<(), Self::Error> { + self.storage.write_signature_key_pair(public_key, signature_key_pair)?; + self.flush() + } + + fn write_encryption_key_pair< + EncryptionKey: traits::EncryptionKey, + HpkeKeyPair: traits::HpkeKeyPair, + >( + &self, + public_key: &EncryptionKey, + key_pair: &HpkeKeyPair, + ) -> Result<(), Self::Error> { + self.storage.write_encryption_key_pair(public_key, key_pair)?; + self.flush() + } + + fn write_encryption_epoch_key_pairs< + GroupId: traits::GroupId, + EpochKey: traits::EpochKey, + HpkeKeyPair: traits::HpkeKeyPair, + >( + &self, + group_id: &GroupId, + epoch: &EpochKey, + leaf_index: u32, + key_pairs: &[HpkeKeyPair], + ) -> Result<(), Self::Error> { + self.storage.write_encryption_epoch_key_pairs(group_id, epoch, leaf_index, key_pairs)?; + self.flush() + } + + fn write_key_package< + HashReference: traits::HashReference, + KeyPackage: traits::KeyPackage, + >( + &self, + hash_ref: &HashReference, + key_package: &KeyPackage, + ) -> Result<(), Self::Error> { + self.storage.write_key_package(hash_ref, key_package)?; + self.flush() + } + + fn write_psk< + PskId: traits::PskId, + PskBundle: traits::PskBundle, + >( + &self, + psk_id: &PskId, + psk: &PskBundle, + ) -> Result<(), Self::Error> { + self.storage.write_psk(psk_id, psk)?; + self.flush() + } + + // --- getters (no flush needed) --- + + fn mls_group_join_config< + GroupId: traits::GroupId, + MlsGroupJoinConfig: traits::MlsGroupJoinConfig, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.mls_group_join_config(group_id)?) + } + + fn own_leaf_nodes< + GroupId: traits::GroupId, + LeafNode: traits::LeafNode, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.own_leaf_nodes(group_id)?) + } + + fn queued_proposal_refs< + GroupId: traits::GroupId, + ProposalRef: traits::ProposalRef, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.queued_proposal_refs(group_id)?) + } + + fn queued_proposals< + GroupId: traits::GroupId, + ProposalRef: traits::ProposalRef, + QueuedProposal: traits::QueuedProposal, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.queued_proposals(group_id)?) + } + + fn tree< + GroupId: traits::GroupId, + TreeSync: traits::TreeSync, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.tree(group_id)?) + } + + fn group_context< + GroupId: traits::GroupId, + GroupContext: traits::GroupContext, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.group_context(group_id)?) + } + + fn interim_transcript_hash< + GroupId: traits::GroupId, + InterimTranscriptHash: traits::InterimTranscriptHash, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.interim_transcript_hash(group_id)?) + } + + fn confirmation_tag< + GroupId: traits::GroupId, + ConfirmationTag: traits::ConfirmationTag, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.confirmation_tag(group_id)?) + } + + fn group_state< + GroupState: traits::GroupState, + GroupId: traits::GroupId, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.group_state(group_id)?) + } + + fn message_secrets< + GroupId: traits::GroupId, + MessageSecrets: traits::MessageSecrets, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.message_secrets(group_id)?) + } + + fn resumption_psk_store< + GroupId: traits::GroupId, + ResumptionPskStore: traits::ResumptionPskStore, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.resumption_psk_store(group_id)?) + } + + fn own_leaf_index< + GroupId: traits::GroupId, + LeafNodeIndex: traits::LeafNodeIndex, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.own_leaf_index(group_id)?) + } + + fn group_epoch_secrets< + GroupId: traits::GroupId, + GroupEpochSecrets: traits::GroupEpochSecrets, + >( + &self, + group_id: &GroupId, + ) -> Result, Self::Error> { + Ok(self.storage.group_epoch_secrets(group_id)?) + } + + fn signature_key_pair< + SignaturePublicKey: traits::SignaturePublicKey, + SignatureKeyPair: traits::SignatureKeyPair, + >( + &self, + public_key: &SignaturePublicKey, + ) -> Result, Self::Error> { + Ok(self.storage.signature_key_pair(public_key)?) + } + + fn encryption_key_pair< + HpkeKeyPair: traits::HpkeKeyPair, + EncryptionKey: traits::EncryptionKey, + >( + &self, + public_key: &EncryptionKey, + ) -> Result, Self::Error> { + Ok(self.storage.encryption_key_pair(public_key)?) + } + + fn encryption_epoch_key_pairs< + GroupId: traits::GroupId, + EpochKey: traits::EpochKey, + HpkeKeyPair: traits::HpkeKeyPair, + >( + &self, + group_id: &GroupId, + epoch: &EpochKey, + leaf_index: u32, + ) -> Result, Self::Error> { + Ok(self.storage.encryption_epoch_key_pairs(group_id, epoch, leaf_index)?) + } + + fn key_package< + KeyPackageRef: traits::HashReference, + KeyPackage: traits::KeyPackage, + >( + &self, + hash_ref: &KeyPackageRef, + ) -> Result, Self::Error> { + Ok(self.storage.key_package(hash_ref)?) + } + + fn psk< + PskBundle: traits::PskBundle, + PskId: traits::PskId, + >( + &self, + psk_id: &PskId, + ) -> Result, Self::Error> { + Ok(self.storage.psk(psk_id)?) + } + + // --- deleters (flush needed) --- + + fn remove_proposal< + GroupId: traits::GroupId, + ProposalRef: traits::ProposalRef, + >( + &self, + group_id: &GroupId, + proposal_ref: &ProposalRef, + ) -> Result<(), Self::Error> { + self.storage.remove_proposal(group_id, proposal_ref)?; + self.flush() + } + + fn delete_own_leaf_nodes>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_own_leaf_nodes(group_id)?; + self.flush() + } + + fn delete_group_config>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_group_config(group_id)?; + self.flush() + } + + fn delete_tree>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_tree(group_id)?; + self.flush() + } + + fn delete_confirmation_tag>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_confirmation_tag(group_id)?; + self.flush() + } + + fn delete_group_state>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_group_state(group_id)?; + self.flush() + } + + fn delete_context>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_context(group_id)?; + self.flush() + } + + fn delete_interim_transcript_hash>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_interim_transcript_hash(group_id)?; + self.flush() + } + + fn delete_message_secrets>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_message_secrets(group_id)?; + self.flush() + } + + fn delete_all_resumption_psk_secrets>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_all_resumption_psk_secrets(group_id)?; + self.flush() + } + + fn delete_own_leaf_index>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_own_leaf_index(group_id)?; + self.flush() + } + + fn delete_group_epoch_secrets>( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.delete_group_epoch_secrets(group_id)?; + self.flush() + } + + fn clear_proposal_queue< + GroupId: traits::GroupId, + ProposalRef: traits::ProposalRef, + >( + &self, + group_id: &GroupId, + ) -> Result<(), Self::Error> { + self.storage.clear_proposal_queue::(group_id)?; + self.flush() + } + + fn delete_signature_key_pair< + SignaturePublicKey: traits::SignaturePublicKey, + >( + &self, + public_key: &SignaturePublicKey, + ) -> Result<(), Self::Error> { + self.storage.delete_signature_key_pair(public_key)?; + self.flush() + } + + fn delete_encryption_key_pair>( + &self, + public_key: &EncryptionKey, + ) -> Result<(), Self::Error> { + self.storage.delete_encryption_key_pair(public_key)?; + self.flush() + } + + fn delete_encryption_epoch_key_pairs< + GroupId: traits::GroupId, + EpochKey: traits::EpochKey, + >( + &self, + group_id: &GroupId, + epoch: &EpochKey, + leaf_index: u32, + ) -> Result<(), Self::Error> { + self.storage.delete_encryption_epoch_key_pairs(group_id, epoch, leaf_index)?; + self.flush() + } + + fn delete_key_package>( + &self, + hash_ref: &KeyPackageRef, + ) -> Result<(), Self::Error> { + self.storage.delete_key_package(hash_ref)?; + self.flush() + } + + fn delete_psk>( + &self, + psk_id: &PskKey, + ) -> Result<(), Self::Error> { + self.storage.delete_psk(psk_id)?; self.flush() } } diff --git a/crates/quicprochat-sdk/src/groups.rs b/crates/quicprochat-sdk/src/groups.rs index 338ab02..61ffbff 100644 --- a/crates/quicprochat-sdk/src/groups.rs +++ b/crates/quicprochat-sdk/src/groups.rs @@ -77,10 +77,8 @@ pub async fn create_dm( // Save conversation with MLS state. let member_keys = member.member_identities(); let mls_blob = member - .group_ref() - .map(bincode::serialize) - .transpose() - .map_err(|e| SdkError::Storage(format!("serialize MLS group: {e}")))?; + .serialize_mls_state() + .map_err(|e| SdkError::Storage(format!("serialize MLS state: {e}")))?; let conv = Conversation { id: conv_id.clone(), @@ -147,10 +145,8 @@ pub fn create_group( let member_keys = member.member_identities(); let mls_blob = member - .group_ref() - .map(bincode::serialize) - .transpose() - .map_err(|e| SdkError::Storage(format!("serialize MLS group: {e}")))?; + .serialize_mls_state() + .map_err(|e| SdkError::Storage(format!("serialize MLS state: {e}")))?; let conv = Conversation { id: conv_id.clone(), @@ -252,10 +248,8 @@ pub fn join_from_welcome( let member_keys = member.member_identities(); let mls_blob = member - .group_ref() - .map(bincode::serialize) - .transpose() - .map_err(|e| SdkError::Storage(format!("serialize MLS group: {e}")))?; + .serialize_mls_state() + .map_err(|e| SdkError::Storage(format!("serialize MLS state: {e}")))?; // Upsert conversation — the stub may already exist from create_dm. let existing = conv_store @@ -509,10 +503,8 @@ pub fn save_mls_state( .ok_or_else(|| SdkError::ConversationNotFound(conv_id.hex()))?; conv.mls_group_blob = member - .group_ref() - .map(bincode::serialize) - .transpose() - .map_err(|e| SdkError::Storage(format!("serialize MLS group: {e}")))?; + .serialize_mls_state() + .map_err(|e| SdkError::Storage(format!("serialize MLS state: {e}")))?; conv.member_keys = member.member_identities(); conv.is_hybrid = member.is_hybrid(); @@ -529,16 +521,19 @@ pub fn restore_mls_state( conv: &Conversation, identity: &Arc, ) -> Result { - let group_blob = conv + let storage_blob = conv .mls_group_blob .as_ref() .ok_or_else(|| SdkError::Crypto("no MLS group blob in conversation".into()))?; - let mls_group = bincode::deserialize(group_blob) - .map_err(|e| SdkError::Crypto(format!("deserialize MLS group: {e}")))?; - - let ks = quicprochat_core::DiskKeyStore::ephemeral(); - let member = GroupMember::new_with_state(Arc::clone(identity), ks, Some(mls_group), conv.is_hybrid); + let group_id = conv.id.0.as_slice(); + let member = GroupMember::new_from_storage_bytes( + Arc::clone(identity), + storage_blob, + group_id, + conv.is_hybrid, + ) + .map_err(|e| SdkError::Crypto(format!("restore MLS state: {e}")))?; Ok(member) } diff --git a/crates/quicprochat-server/Cargo.toml b/crates/quicprochat-server/Cargo.toml index fa2afa2..39904c0 100644 --- a/crates/quicprochat-server/Cargo.toml +++ b/crates/quicprochat-server/Cargo.toml @@ -65,7 +65,7 @@ serde_json = { workspace = true } # CLI clap = { workspace = true } -toml = { version = "0.8" } +toml = { workspace = true } # WebSocket JSON-RPC bridge for browser clients tokio-tungstenite = "0.26" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a58aeee..32e9409 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -6,6 +6,7 @@ # Why quicprochat? +- [How quicprochat Compares to WhatsApp, Telegram, and Signal](design-rationale/messenger-comparison.md) - [Comparison with Classical Chat Protocols](design-rationale/protocol-comparison.md) - [Why This Design, Not Signal/Matrix/...](design-rationale/why-not-signal.md) diff --git a/examples/plugins/logging_plugin/Cargo.lock b/examples/plugins/logging_plugin/Cargo.lock index 2bcb139..892d2cc 100644 --- a/examples/plugins/logging_plugin/Cargo.lock +++ b/examples/plugins/logging_plugin/Cargo.lock @@ -6,9 +6,9 @@ version = 4 name = "logging_plugin" version = "0.1.0" dependencies = [ - "quicproquo-plugin-api", + "quicprochat-plugin-api", ] [[package]] -name = "quicproquo-plugin-api" +name = "quicprochat-plugin-api" version = "0.1.0" diff --git a/examples/plugins/rate_limit_plugin/Cargo.lock b/examples/plugins/rate_limit_plugin/Cargo.lock index 64124c0..55ae5dd 100644 --- a/examples/plugins/rate_limit_plugin/Cargo.lock +++ b/examples/plugins/rate_limit_plugin/Cargo.lock @@ -3,12 +3,12 @@ version = 4 [[package]] -name = "quicproquo-plugin-api" +name = "quicprochat-plugin-api" version = "0.1.0" [[package]] name = "rate_limit_plugin" version = "0.1.0" dependencies = [ - "quicproquo-plugin-api", + "quicprochat-plugin-api", ] diff --git a/sdks/typescript/wasm-crypto/Cargo.lock b/sdks/typescript/wasm-crypto/Cargo.lock index babb963..fae4f91 100644 --- a/sdks/typescript/wasm-crypto/Cargo.lock +++ b/sdks/typescript/wasm-crypto/Cargo.lock @@ -425,7 +425,7 @@ dependencies = [ ] [[package]] -name = "qpq-wasm-crypto" +name = "qpc-wasm-crypto" version = "0.1.0" dependencies = [ "js-sys",