diff --git a/Cargo.lock b/Cargo.lock index cc07742..9650395 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.13.3" +version = "3.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11004b0e9b44b4eb3d15e0c3132b96fb178c7e50a74758b2f17bb9cc9a7fb4f6" +checksum = "53200bd1513e569e6e644181c922cec072c121a27db5b38d45e88e630c369366" dependencies = [ "actix-codec", "actix-rt", @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.14.1" +version = "4.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58356675d8c86d2e720480645a0316808471a62d0073f6a3b98810a5e0ca0e73" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" dependencies = [ "actix-codec", "actix-http", @@ -410,18 +410,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures 0.2.17", - "password-hash", -] - [[package]] name = "asn1-rs" version = "0.7.2" @@ -434,7 +422,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.20", + "thiserror 2.0.19", "time", ] @@ -463,9 +451,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.92" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", @@ -571,15 +559,6 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -[[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 = "block-buffer" version = "0.10.4" @@ -678,9 +657,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "9066c49992464636f92905fa096ec58baaa4d57ec19a5c096c68d3e25ef3d136" dependencies = [ "find-msvc-tools", "jobserver", @@ -1514,9 +1493,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "finl_unicode" @@ -1585,9 +1564,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1600,9 +1579,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1610,15 +1589,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1627,38 +1606,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1757,9 +1736,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.16" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1969,9 +1948,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -2012,7 +1991,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.16", + "h2 0.4.15", "http 1.5.0", "http-body", "httparse", @@ -2090,9 +2069,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", @@ -2104,9 +2083,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2117,9 +2096,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2131,17 +2110,16 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2152,15 +2130,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2384,7 +2362,6 @@ dependencies = [ "iota-storage", "iota-util", "mtp", - "tokio", ] [[package]] @@ -2540,7 +2517,7 @@ dependencies = [ "serde_yaml", "sha2 0.11.0", "sysinfo", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "uuid", "walkdir", @@ -2673,7 +2650,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.20", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2722,9 +2699,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -2745,7 +2722,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] @@ -2805,9 +2782,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libsqlite3-sys" -version = "0.38.2" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" dependencies = [ "pkg-config", "vcpkg", @@ -2830,9 +2807,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -3027,8 +3004,8 @@ dependencies = [ [[package]] name = "mtp" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-client", "mtp-codec", @@ -3043,8 +3020,8 @@ dependencies = [ [[package]] name = "mtp-client" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-codec", "mtp-common", @@ -3056,8 +3033,8 @@ dependencies = [ [[package]] name = "mtp-codec" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "base64 0.23.1", "byteorder", @@ -3065,27 +3042,25 @@ dependencies = [ "mtp-crypto", "mtp-type-map", "rand 0.10.2", - "thiserror 2.0.20", ] [[package]] name = "mtp-common" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "quinn", "rustls", - "thiserror 2.0.20", + "thiserror 2.0.19", "wtransport", ] [[package]] name = "mtp-crypto" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ - "argon2", - "base64 0.22.1", + "base64 0.23.1", "chacha20poly1305", "ed25519-dalek 3.0.0", "getrandom 0.4.3", @@ -3093,37 +3068,36 @@ dependencies = [ "ml-dsa", "mlkem-tls", "rand 0.10.2", - "rand_core 0.6.4", + "rand_core 0.10.1", "rustls", "serde", "sha2 0.11.0", - "thiserror 1.0.69", + "thiserror 2.0.19", "tokio", "zeroize", ] [[package]] name = "mtp-files" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-crypto", "rand 0.10.2", - "thiserror 2.0.20", + "thiserror 2.0.19", "zeroize", ] [[package]] name = "mtp-host" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-codec", "mtp-common", "mtp-crypto", "mtp-transport", "rand 0.10.2", - "thiserror 2.0.20", "tokio", "tracing", "wtransport", @@ -3131,14 +3105,13 @@ dependencies = [ [[package]] name = "mtp-transport" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "async-trait", "mtp-codec", "mtp-common", "mtp-crypto", - "rand 0.10.2", "rcgen", "rustls", "rustls-native-certs", @@ -3146,13 +3119,12 @@ dependencies = [ "tokio", "tracing", "wtransport", - "zeroize", ] [[package]] name = "mtp-type-map" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "serde", "serde_yaml", @@ -3160,8 +3132,8 @@ dependencies = [ [[package]] name = "mtp-webserver" -version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "async-trait", "bytes", @@ -3178,8 +3150,9 @@ dependencies = [ "mtp-host", "mtp-transport", "quinn", + "rand 0.10.2", "rustls", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "tokio-rustls", "tokio-stream", @@ -3270,9 +3243,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.47" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] @@ -3579,17 +3552,6 @@ 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 = "pbkdf2" version = "0.12.2" @@ -3627,9 +3589,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.0" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -3637,9 +3599,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.0" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -3647,9 +3609,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.0" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", @@ -3660,9 +3622,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.9.0" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", ] @@ -3767,9 +3729,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "pnet" @@ -3887,15 +3849,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.15.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "potential_utf" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3945,7 +3907,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -3953,9 +3915,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.17" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", @@ -3970,7 +3932,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.20", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -4116,7 +4078,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.20", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -4187,9 +4149,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.9" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "aws-lc-rs", "pem", @@ -4254,7 +4216,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "h2 0.4.16", + "h2 0.4.15", "http 1.5.0", "http-body", "http-body-util", @@ -4303,14 +4265,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] name = "rusqlite" -version = "0.40.2" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" dependencies = [ "bitflags 2.13.1", "fallible-iterator", @@ -4434,9 +4396,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -5035,11 +4997,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.19", ] [[package]] @@ -5055,9 +5017,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", @@ -5098,9 +5060,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -5306,7 +5268,7 @@ dependencies = [ "native-tls", "rand 0.10.2", "sha1 0.11.0", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] @@ -5416,9 +5378,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", "getrandom 0.4.3", @@ -5510,9 +5472,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -5523,9 +5485,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5533,9 +5495,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5543,9 +5505,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -5556,9 +5518,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -5578,9 +5540,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -5970,15 +5932,15 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wtransport" -version = "0.7.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036" +checksum = "ea4aacf790813ee1956751491800537f4e04af7557b7b370501ccbfbc85963e4" dependencies = [ "bytes", "pem", @@ -5989,7 +5951,7 @@ dependencies = [ "rustls-pki-types", "sha2 0.11.0", "socket2", - "thiserror 2.0.20", + "thiserror 2.0.19", "time", "tokio", "tracing", @@ -6000,13 +5962,13 @@ dependencies = [ [[package]] name = "wtransport-proto" -version = "0.7.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad9059572c7dbd6901ccef37f3b7321678cd708dcf58a64b1921dbeab7bfede" +checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" dependencies = [ "httlib-huffman", "octets", - "thiserror 2.0.20", + "thiserror 2.0.19", "url", ] @@ -6048,7 +6010,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.20", + "thiserror 2.0.19", "time", ] @@ -6148,9 +6110,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6159,9 +6121,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.7" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6170,13 +6132,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.5" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 2.0.119", ] [[package]] diff --git a/client/Cargo.toml b/client/Cargo.toml index a7ff680..605e87b 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["client", "crypto"] } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["client"] } iota-connection = { path = "../iota-connection" } iota-logger = { path = "../iota-logger" } iota-util = { path = "../iota-util" } diff --git a/client/src/client_connection.rs b/client/src/client_connection.rs index a90df86..512cc31 100644 --- a/client/src/client_connection.rs +++ b/client/src/client_connection.rs @@ -1,14 +1,14 @@ use dashmap::DashMap; use iota_connection::message_common::*; use iota_connection::message_handlers; -use iota_connection::relay::message_security_class; use iota_logger::{log_cv_in, log_cv_out, log_t}; +use iota_storage::util::chat_files::{self, MessageState, change_message_state}; use iota_storage::util::config_util::CONFIG; +use iota_storage::util::e2ee_storage::{self, StoredChatSecret}; use iota_util::crypto_helper::keyring_from_base64; use iota_util::crypto_util::{self}; use mtp::client::{Receiver, Sender}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; -use mtp::crypto::Keyring; use std::sync::Arc; use std::time::Duration; @@ -30,7 +30,6 @@ pub struct ClientConnection { pub waiting_tasks: DashMap, CommunicationValue) -> bool + Send + Sync>>, shutdown: Arc>, - keyring: Arc>>>, } impl ClientConnection { @@ -54,31 +53,9 @@ impl ClientConnection { shutdown_tx, waiting_tasks, shutdown, - keyring: Arc::new(RwLock::new(None)), } } - pub async fn set_keyring(&self, keyring: Arc) { - *self.keyring.write().await = Some(keyring); - } - - async fn local_keyring(&self) -> Result, String> { - if let Some(keyring) = self.keyring.read().await.as_ref().cloned() { - return Ok(keyring); - } - - let keyring_data = CONFIG - .load() - .keyring - .clone() - .ok_or_else(|| "Iota keyring is not configured".to_string())?; - let keyring = keyring_from_base64(&keyring_data) - .ok_or_else(|| "Iota keyring is invalid".to_string())?; - let keyring = Arc::new(keyring); - *self.keyring.write().await = Some(keyring.clone()); - Ok(keyring) - } - pub fn start(self: Arc) { let self_clone = self.clone(); tokio::spawn(async move { @@ -119,27 +96,6 @@ impl ClientConnection { pub async fn handle_message(self: Arc, cv: CommunicationValue) { log_cv_in!(&cv); - if cv.is_type(CommunicationType::Relay) { - log_t!( - "relay_from_client_rejected", - "legacy client path has no Relay router".to_string() - ); - let _ = self - .send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - } - - if matches!( - message_security_class(&cv), - iota_connection::relay::MessageSecurityClass::RelayOnly - ) { - let _ = self - .send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - } - let _msg_id = cv.get_id(); if cv.is_type(CommunicationType::Challenge) { @@ -147,12 +103,82 @@ impl ClientConnection { return; } + if cv.is_type(CommunicationType::SetChatSecret) { + let sender_id = cv.get_sender().to_string(); + let recipients = match chat_secret_recipients(&cv) { + Some(recipients) => recipients, + None => { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; + let now = now_millis_i64(); + let chat_id = data_string(&cv, DataType::ChatId); + let secret_id = data_string(&cv, DataType::SecretId); + let version = data_i64(&cv, DataType::VersionNumber); + let wrapping_scheme = data_string(&cv, DataType::WrappingScheme); + let created_at = data_i64(&cv, DataType::CreatedAt).unwrap_or(now); + + let Some((((chat_id, secret_id), version), wrapping_scheme)) = + chat_id.zip(secret_id).zip(version).zip(wrapping_scheme) + else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + + for recipient in recipients.iter().filter(|item| item.user_id == sender_id) { + if e2ee_storage::put_chat_secret(StoredChatSecret { + user_id: recipient.user_id.clone(), + chat_id: chat_id.clone(), + secret_id: secret_id.clone(), + version, + encrypted_secret: recipient.encrypted_secret.clone(), + kem_ciphertext: recipient.kem_ciphertext.clone(), + wrapping_scheme: wrapping_scheme.clone(), + created_at, + updated_at: now, + }) + .is_err() + { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + } + + for recipient in recipients.iter().filter(|item| item.user_id != sender_id) { + self.send_message(&set_chat_secret_cv_for_recipient(&cv, recipient)) + .await; + } + + self.send_message(&error_response(&cv, CommunicationType::Success)) + .await; + return; + } + if cv.is_type(CommunicationType::GetChatSecret) { self.send_message(&message_handlers::handle_get_chat_secret(&cv)) .await; return; } + if cv.is_type(CommunicationType::ChatSecretForward) { + let sender_id = cv.get_sender().to_string(); + let recipient_id = data_string(&cv, DataType::RecipientUserId).unwrap_or_default(); + if data_string(&cv, DataType::SenderUserId).as_deref() != Some(sender_id.as_str()) + || recipient_id.is_empty() + { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + self.send_message(&cv.with_receiver(recipient_id.parse::().unwrap_or(0))) + .await; + return; + } + if cv.is_type(CommunicationType::SaveAppData) { let sender_id = cv.get_sender(); let _app_data = cv @@ -244,6 +270,137 @@ impl ClientConnection { return; } + if cv.is_type(CommunicationType::MessageOtherIota) { + let sender_id = &cv.get_sender(); + let receiver_id = &cv.get_receiver(); + + // parse send_time safely (number or string), fallback to now + let send_time_val = cv.get_data(DataType::SendTime); + let now_i64 = now_millis_i64(); + let timestamp = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + + // content may be missing or non-string; default to empty string + let content = cv + .get_data(DataType::Content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; + let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); + + chat_files::add_message( + timestamp as u128, + false, + *receiver_id as i64, + *sender_id as i64, + &content, + height, + reply_to, + ); + + // Build user_forward using the parsed numeric timestamp and safe content string + let user_forward = CommunicationValue::new(CommunicationType::MessageLive) + .with_id(cv.get_id()) + .with_receiver(*receiver_id) + .add_typed_default( + DataType::SenderId, + DataValue::SignedNumber(*sender_id as i128), + ) + .add_typed_default(DataType::Message, { + let mut msg_fields = vec![ + (DataType::Content, DataValue::Str(content.clone())), + ( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ), + (DataType::Height, DataValue::SignedNumber(height as i128)), + ]; + if let Some(rt) = reply_to { + msg_fields.push(( + DataType::ReplyId, + DataValue::UnsignedNumber(rt as u64 as u128), + )); + } + typed_container(msg_fields) + }); + + let user_resp = self + .clone() + .await_response(&user_forward, Some(Duration::from_secs(10))) + .await; + + if let Ok(user_resp) = user_resp { + let ms_raw = user_resp + .get_data(DataType::MessageState) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + let _ = change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + ms.clone(), + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(*sender_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } else { + // Delivery timed out/failed — update stored state and notify sender with numeric timestamp + let _ = chat_files::change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + MessageState::Sent, + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(*receiver_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + return; + } + if cv.is_type(CommunicationType::MessagesGet) { self.send_message(&message_handlers::handle_messages_get(&cv)) .await; @@ -346,7 +503,9 @@ impl ClientConnection { } async fn handle_challenge(&self, cv: &CommunicationValue) { - let Ok(keyring) = self.local_keyring().await else { + let kr_str = CONFIG.load().keyring.clone().unwrap(); + + let Some(keyring) = keyring_from_base64(&kr_str) else { return; }; diff --git a/communities/src/community_connection.rs b/communities/src/community_connection.rs index 685d9d8..36a23c0 100644 --- a/communities/src/community_connection.rs +++ b/communities/src/community_connection.rs @@ -2,7 +2,6 @@ use crate::auth::auth_user::AuthUser; use crate::communities::community::Community; use crate::communities::interactables::interactable::Interactable; use crate::users::user_manager::get_user; -use iota_util::mtp_compat::CommunicationValueCompat; use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; use base64::{Engine as _, engine::general_purpose::STANDARD}; use futures::SinkExt; diff --git a/communities/src/interactables/text_chat.rs b/communities/src/interactables/text_chat.rs index 6b69baa..967f27b 100644 --- a/communities/src/interactables/text_chat.rs +++ b/communities/src/interactables/text_chat.rs @@ -8,7 +8,6 @@ use crate::{ }; use async_trait::async_trait; use json::{JsonValue, array, object}; -use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt}; use std::fs; use std::path::Path; use std::sync::Arc; diff --git a/communities/src/interactables/voice_chat.rs b/communities/src/interactables/voice_chat.rs index 7e20022..5e83181 100644 --- a/communities/src/interactables/voice_chat.rs +++ b/communities/src/interactables/voice_chat.rs @@ -1,7 +1,6 @@ use crate::communities::{community::Community, interactables::interactable::Interactable}; use async_trait::async_trait; use json::JsonValue; -use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt}; use std::sync::Arc; use std::{any::Any, sync::RwLock}; use uuid::Uuid; diff --git a/iota-cli/Cargo.toml b/iota-cli/Cargo.toml index dff217b..581a8c4 100644 --- a/iota-cli/Cargo.toml +++ b/iota-cli/Cargo.toml @@ -23,7 +23,7 @@ iota-process-manager = { path = "../iota-process-manager" } iota-paths = { path = "../iota-paths" } omikron-connector = { path = "../omikron-connector", optional = true } -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true } actix-web = { version = "4", features = ["rustls-0_23"] } diff --git a/iota-cli/src/controls/dialog.rs b/iota-cli/src/controls/dialog.rs index 3756641..eb750ef 100644 --- a/iota-cli/src/controls/dialog.rs +++ b/iota-cli/src/controls/dialog.rs @@ -189,7 +189,9 @@ impl Screen for ConfirmDialog { let button_widths: Vec = self .buttons .iter() - .map(|b| crate::controls::button::button_minimum_width(&b.label)) + .map(|b| { + crate::controls::button::button_minimum_width(&b.label) + }) .collect(); let total_width: u16 = button_widths.iter().sum(); @@ -198,7 +200,9 @@ impl Screen for ConfirmDialog { let start_x = buttons_area.x + available.saturating_sub(total_width + spacing) / 2; let mut x = start_x; - for (i, (button_config, &width)) in self.buttons.iter().zip(&button_widths).enumerate() { + for (i, (button_config, &width)) in + self.buttons.iter().zip(&button_widths).enumerate() + { let button_area = Rect { x, y: buttons_area.y, diff --git a/iota-cli/src/controls/header.rs b/iota-cli/src/controls/header.rs index c38eda4..09a589f 100644 --- a/iota-cli/src/controls/header.rs +++ b/iota-cli/src/controls/header.rs @@ -11,17 +11,12 @@ use ratatui::{ widgets::Paragraph, }; -fn connection_badge( - state: &IpcConnectionState, - theme: &ResolvedTheme, -) -> (&'static str, ratatui::style::Style) { +fn connection_badge(state: &IpcConnectionState, theme: &ResolvedTheme) -> (&'static str, ratatui::style::Style) { match state { IpcConnectionState::Connected => ("OK", theme.status.success), IpcConnectionState::Connecting => ("..", theme.status.warning), IpcConnectionState::Reconnecting { .. } => ("WARN", theme.status.warning), - IpcConnectionState::Failed { .. } | IpcConnectionState::Incompatible { .. } => { - ("FAIL", theme.status.error) - } + IpcConnectionState::Failed { .. } | IpcConnectionState::Incompatible { .. } => ("FAIL", theme.status.error), IpcConnectionState::Disconnected => ("WARN", theme.status.warning), } } @@ -57,8 +52,7 @@ pub fn render_header( format!(" v{}", daemon.version) }; - let rows = - Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area); + let rows = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area); let cells = Layout::horizontal([ Constraint::Min(28), Constraint::Length(12), @@ -100,34 +94,10 @@ pub fn render_header( hits.register(brand_area, AppAction::OpenMain); for (index, (top, _bottom, label, intent, action)) in [ - ( - cells[1], - cells2[1], - "Overview", - ButtonIntent::Primary, - AppAction::OpenOverview, - ), - ( - cells[2], - cells2[2], - "Users", - ButtonIntent::Neutral, - AppAction::OpenUsers, - ), - ( - cells[3], - cells2[3], - "Settings", - ButtonIntent::Neutral, - AppAction::OpenSettings, - ), - ( - cells[4], - cells2[4], - "Quit", - ButtonIntent::Destructive, - AppAction::Quit, - ), + (cells[1], cells2[1], "Overview", ButtonIntent::Primary, AppAction::OpenOverview), + (cells[2], cells2[2], "Users", ButtonIntent::Neutral, AppAction::OpenUsers), + (cells[3], cells2[3], "Settings", ButtonIntent::Neutral, AppAction::OpenSettings), + (cells[4], cells2[4], "Quit", ButtonIntent::Destructive, AppAction::Quit), ] .into_iter() .enumerate() diff --git a/iota-cli/src/screens/settings.rs b/iota-cli/src/screens/settings.rs index a0a37d8..56293c5 100644 --- a/iota-cli/src/screens/settings.rs +++ b/iota-cli/src/screens/settings.rs @@ -186,7 +186,8 @@ impl Screen for SettingsScreen { let inner = header_block.inner(area); frame.render_widget(header_block, area); - let sections = Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).split(inner); + let sections = Layout::vertical([Constraint::Length(2), Constraint::Min(1)]) + .split(inner); frame.render_widget( Paragraph::new(format!( diff --git a/iota-cli/src/screens/users.rs b/iota-cli/src/screens/users.rs index 1ff2689..93d4b13 100644 --- a/iota-cli/src/screens/users.rs +++ b/iota-cli/src/screens/users.rs @@ -122,7 +122,10 @@ impl UsersScreen { if self.loading { const SPINNERS: &[u8] = b"|/-\\"; let ch = SPINNERS[self.tick.fetch_add(1, Ordering::Relaxed) as usize % SPINNERS.len()]; - f.render_widget(Paragraph::new(format!("{ch} Loading users…")), inner); + f.render_widget( + Paragraph::new(format!("{ch} Loading users…")), + inner, + ); return; } if visible_indices.is_empty() { @@ -146,20 +149,9 @@ impl UsersScreen { let user = &self.users[*user_index]; ( *user_index, - format!( - "{:>6} {} {}{}", - user.user_id, - user.username, - match user.state { - iota_ipc::LocalUserState::Managed => "managed", - iota_ipc::LocalUserState::Released => "released", - }, - if user.data_present { - "" - } else { - ", data purged" - } - ), + format!("{:>6} {} {}{}", user.user_id, user.username, + match user.state { iota_ipc::LocalUserState::Managed => "managed", iota_ipc::LocalUserState::Released => "released" }, + if user.data_present { "" } else { ", data purged" }), ) }) .collect(); @@ -541,14 +533,11 @@ impl Screen for UsersScreen { match result { Ok(()) => { self.pending_dialog = None; - if let Some(user) = - self.users.iter_mut().find(|user| user.user_id == user_id) - { + if let Some(user) = self.users.iter_mut().find(|user| user.user_id == user_id) { user.state = iota_ipc::LocalUserState::Released; user.credential_present = false; } - self.message = - Some(format!("Released user {user_id}; hosted data retained.")); + self.message = Some(format!("Released user {user_id}; hosted data retained.")); } Err(error) => { self.dialog = self.pending_dialog.take(); diff --git a/iota-cli/src/ui.rs b/iota-cli/src/ui.rs index ece8697..dad4c80 100644 --- a/iota-cli/src/ui.rs +++ b/iota-cli/src/ui.rs @@ -306,10 +306,7 @@ impl UI { } pub async fn notifications(&self) -> Vec { - self.notifications - .lock() - .map(|n| n.clone()) - .unwrap_or_default() + self.notifications.lock().map(|n| n.clone()).unwrap_or_default() } pub async fn set_screen(&self, screen: Box) { @@ -614,9 +611,7 @@ impl UI { Ok(iota_ipc::ResponseResult::Error(error)) => { Err(format!("Cannot load users: {error}")) } - Ok(_) => { - Err("Daemon returned an unexpected response while loading users.".into()) - } + Ok(_) => Err("Daemon returned an unexpected response while loading users.".into()), Err(error) => Err(format!("Cannot load users: {error}")), } }; @@ -710,12 +705,7 @@ impl UI { width: 40.min(rows[1].width), height: 3.min(rows[1].height), }; - render_notification_area( - f, - notification_area, - ¬ifications, - context.theme, - ); + render_notification_area(f, notification_area, ¬ifications, context.theme); } } })?; diff --git a/iota-connection/Cargo.toml b/iota-connection/Cargo.toml index 0148648..65eafad 100644 --- a/iota-connection/Cargo.toml +++ b/iota-connection/Cargo.toml @@ -6,7 +6,4 @@ edition = "2024" [dependencies] iota-storage = { path = "../iota-storage" } iota-util = { path = "../iota-util" } -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["crypto"] } - -[dev-dependencies] -tokio = { version = "1.50.0", features = ["macros", "rt"] } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } diff --git a/iota-connection/src/lib.rs b/iota-connection/src/lib.rs index db303ab..601ca7b 100644 --- a/iota-connection/src/lib.rs +++ b/iota-connection/src/lib.rs @@ -1,4 +1,3 @@ pub mod connection_handler; pub mod message_common; pub mod message_handlers; -pub mod relay; diff --git a/iota-connection/src/message_common.rs b/iota-connection/src/message_common.rs index fff5bf6..2ab9733 100644 --- a/iota-connection/src/message_common.rs +++ b/iota-connection/src/message_common.rs @@ -2,8 +2,6 @@ use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::type_map::TypeMap; use std::time::{SystemTime, UNIX_EPOCH}; -pub use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt}; - pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { use mtp::type_map::{DataTypeId, TypeMap}; let tm = TypeMap::latest(); @@ -17,34 +15,22 @@ pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { pub fn data_string(cv: &CommunicationValue, dt: DataType) -> Option { cv.get_data(dt) - .and_then(DataValue::as_str) + .as_str() .map(|s| s.to_string()) - .or_else(|| { - cv.get_data(dt) - .and_then(DataValue::as_number) - .map(|n| n.to_string()) - }) - .or_else(|| { - cv.get_data(dt) - .and_then(DataValue::as_signed_number) - .map(|n| n.to_string()) - }) + .or_else(|| cv.get_data(dt).as_number().map(|n| n.to_string())) + .or_else(|| cv.get_data(dt).as_signed_number().map(|n| n.to_string())) } pub fn data_i64(cv: &CommunicationValue, dt: DataType) -> Option { cv.get_data(dt) - .and_then(DataValue::as_number) + .as_number() .and_then(|n| i64::try_from(n).ok()) .or_else(|| { cv.get_data(dt) - .and_then(DataValue::as_signed_number) + .as_signed_number() .and_then(|n| i64::try_from(n).ok()) }) - .or_else(|| { - cv.get_data(dt) - .and_then(DataValue::as_str) - .and_then(|s| s.parse::().ok()) - }) + .or_else(|| cv.get_data(dt).as_str().and_then(|s| s.parse::().ok())) } #[derive(Debug, Clone)] @@ -81,7 +67,7 @@ pub fn recipient_from_value(value: &DataValue) -> Option { } pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option> { - let recipients = cv.get_data(DataType::Recipients)?.as_array()?; + let recipients = cv.get_data(DataType::Recipients).as_array()?; let parsed = recipients .iter() .map(recipient_from_value) @@ -94,6 +80,49 @@ pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option CommunicationValue { + let recipient_value = typed_container(vec![ + (DataType::UserId, DataValue::Str(recipient.user_id.clone())), + ( + DataType::EncryptedSecret, + DataValue::Bytes(recipient.encrypted_secret.clone()), + ), + ( + DataType::KemCiphertext, + DataValue::Bytes(recipient.kem_ciphertext.clone()), + ), + ]); + + CommunicationValue::new(CommunicationType::SetChatSecret) + .with_id(source.get_id()) + .with_sender(source.get_sender()) + .with_receiver(recipient.user_id.parse::().unwrap_or(0)) + .add_typed_default(DataType::ChatId, source.get_data(DataType::ChatId).clone()) + .add_typed_default( + DataType::SecretId, + source.get_data(DataType::SecretId).clone(), + ) + .add_typed_default( + DataType::VersionNumber, + source.get_data(DataType::VersionNumber).clone(), + ) + .add_typed_default( + DataType::WrappingScheme, + source.get_data(DataType::WrappingScheme).clone(), + ) + .add_typed_default( + DataType::CreatedAt, + source.get_data(DataType::CreatedAt).clone(), + ) + .add_typed_default( + DataType::Recipients, + DataValue::Array(vec![recipient_value]), + ) +} + pub fn now_millis_i64() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -102,9 +131,7 @@ pub fn now_millis_i64() -> i64 { } pub fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue { - let mut response = CommunicationValue::new(ty).with_id(request.id().unwrap_or_default()); - if let Some(sender) = request.sender() { - response = response.with_receiver(sender); - } - response + CommunicationValue::new(ty) + .with_id(request.get_id()) + .with_receiver(request.get_sender()) } diff --git a/iota-connection/src/message_handlers.rs b/iota-connection/src/message_handlers.rs index 4d7144f..dc1d4dc 100644 --- a/iota-connection/src/message_handlers.rs +++ b/iota-connection/src/message_handlers.rs @@ -4,11 +4,7 @@ use iota_storage::util::chats_util::{self, get_user, mod_user}; use iota_storage::util::communities_util::CommunitiesUtil; use iota_storage::util::e2ee_storage::{self, ChatSecretQuery}; use iota_storage::util::settings; -use mtp::codec::{ - CommunicationType, CommunicationValue, DataType, DataValue, TypeMap, VerifiedRelayContent, -}; - -use crate::relay::VerifiedRelayContext; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; pub struct MessageMutation { pub sender_id: i64, @@ -37,170 +33,6 @@ pub fn success_response(cv: &CommunicationValue) -> CommunicationValue { error_response(cv, CommunicationType::Success) } -fn relay_field<'a>( - payload: &'a DataValue, - data_type: DataType, - type_map: &TypeMap, -) -> Option<&'a DataValue> { - payload.get_field(data_type.try_to_id(type_map)?) -} - -fn relay_string<'a>( - payload: &'a DataValue, - data_type: DataType, - type_map: &TypeMap, -) -> Option<&'a str> { - relay_field(payload, data_type, type_map)?.as_str() -} - -fn relay_number(payload: &DataValue, data_type: DataType, type_map: &TypeMap) -> Option { - relay_field(payload, data_type, type_map)?.as_number() -} - -fn relay_identity( - payload: &DataValue, - data_type: DataType, - type_map: &TypeMap, -) -> Result, String> { - let Some(value) = relay_field(payload, data_type, type_map) else { - return Ok(None); - }; - if let Some(number) = value.as_number() { - return u64::try_from(number) - .map(Some) - .map_err(|_| format!("Relay {data_type:?} is outside the user ID range")); - } - if let Some(text) = value.as_str() { - return text - .parse::() - .map(Some) - .map_err(|_| format!("Relay {data_type:?} is not a user ID")); - } - Err(format!("Relay {data_type:?} has an invalid user ID value")) -} - -fn validate_relay_identity( - context: &VerifiedRelayContext, - payload: &DataValue, -) -> Result<(), String> { - if relay_identity(payload, DataType::SenderId, &context.type_map)? - .is_some_and(|sender_id| sender_id != context.signer_id) - { - return Err("Relay SenderId does not match the authenticated signer".into()); - } - if relay_identity(payload, DataType::ReceiverId, &context.type_map)? - .is_some_and(|receiver_id| receiver_id != context.final_recipient_id) - { - return Err("Relay ReceiverId does not match the authenticated recipient".into()); - } - Ok(()) -} - -/* - * Apply only operations whose actor and recipient can be taken from verified - * Relay metadata. The raw Relay frame never enters these handlers, so outer - * routing fields cannot become application identity. - */ -pub fn apply_verified_relay_content( - context: &VerifiedRelayContext, - content: &VerifiedRelayContent, -) -> Result<(), String> { - validate_relay_identity(context, &content.content)?; - let sender_id = i64::try_from(context.signer_id) - .map_err(|_| "Relay signer ID exceeds the local storage range".to_string())?; - let recipient_id = i64::try_from(context.final_recipient_id) - .map_err(|_| "Relay recipient ID exceeds the local storage range".to_string())?; - let created_at = i64::try_from(context.created_at) - .map_err(|_| "Relay creation time exceeds the local storage range".to_string())?; - - match content.message_type.as_str() { - "MessageSend" => { - let message = relay_string(&content.content, DataType::Content, &context.type_map) - .ok_or_else(|| "Relay MessageSend is missing Content".to_string())?; - let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map) - .and_then(|value| i64::try_from(value).ok()) - .unwrap_or(created_at); - let height = relay_number(&content.content, DataType::Height, &context.type_map) - .and_then(|value| i64::try_from(value).ok()) - .unwrap_or_default(); - let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map) - .and_then(|value| i64::try_from(value).ok()); - chat_files::add_message( - u128::try_from(send_time) - .map_err(|_| "Relay MessageSend has a negative SendTime".to_string())?, - false, - recipient_id, - sender_id, - message, - height, - reply_to, - ); - Ok(()) - } - "MessageEdit" => { - let message = relay_string(&content.content, DataType::Content, &context.type_map) - .ok_or_else(|| "Relay MessageEdit is missing Content".to_string())?; - let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map) - .and_then(|value| i64::try_from(value).ok()) - .ok_or_else(|| "Relay MessageEdit is missing SendTime".to_string())?; - chat_files::apply_remote_edit(recipient_id, sender_id, send_time, sender_id, message) - .map_err(|error| error.to_string()) - } - "MessageReactionAdd" | "MessageReactionRemove" => { - let reaction = relay_string(&content.content, DataType::Reaction, &context.type_map) - .filter(|value| !value.is_empty() && value.len() <= 64) - .ok_or_else(|| "Relay reaction is invalid".to_string())?; - let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map) - .and_then(|value| i64::try_from(value).ok()) - .ok_or_else(|| "Relay reaction is missing SendTime".to_string())?; - let result = if content.message_type == "MessageReactionAdd" { - chat_files::add_reaction(recipient_id, sender_id, send_time, sender_id, reaction) - } else { - chat_files::remove_reaction(recipient_id, sender_id, send_time, sender_id, reaction) - }; - result.map_err(|error| error.to_string()) - } - "MessageDeleteLive" => { - let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map) - .and_then(|value| i64::try_from(value).ok()) - .ok_or_else(|| "Relay MessageDeleteLive is missing SendTime".to_string())?; - chat_files::apply_remote_delete(recipient_id, sender_id, send_time, sender_id) - .map_err(|error| error.to_string()) - } - "SetChatSecret" => { - let frame = CommunicationValue::new(CommunicationType::SetChatSecret) - .with_payload(content.content.clone()); - let recipients = chat_secret_recipients(&frame) - .ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?; - let recipient = recipients - .into_iter() - .find(|value| value.user_id == context.final_recipient_id.to_string()) - .ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?; - let chat_id = data_string(&frame, DataType::ChatId) - .ok_or_else(|| "Relay SetChatSecret is missing ChatId".to_string())?; - let secret_id = data_string(&frame, DataType::SecretId) - .ok_or_else(|| "Relay SetChatSecret is missing SecretId".to_string())?; - let version = data_i64(&frame, DataType::VersionNumber) - .ok_or_else(|| "Relay SetChatSecret is missing VersionNumber".to_string())?; - let wrapping_scheme = data_string(&frame, DataType::WrappingScheme) - .ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?; - e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret { - user_id: context.final_recipient_id.to_string(), - chat_id, - secret_id, - version, - encrypted_secret: recipient.encrypted_secret, - kem_ciphertext: recipient.kem_ciphertext, - wrapping_scheme, - created_at, - updated_at: now_millis_i64(), - }) - .map_err(|error| error.to_string()) - } - _ => Ok(()), - } -} - pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue { let mutation = match message_mutation(cv) { Ok(mutation) => mutation, @@ -514,9 +346,7 @@ mod presence_tests { fn sync_error(cv: &CommunicationValue) -> CommunicationValue { error_response(cv, CommunicationType::ErrorInvalidData).add_typed_default( DataType::SessionId, - cv.get_data(DataType::SessionId) - .cloned() - .unwrap_or(DataValue::Null), + cv.get_data(DataType::SessionId).clone(), ) } diff --git a/iota-connection/src/relay.rs b/iota-connection/src/relay.rs deleted file mode 100644 index 298a0df..0000000 --- a/iota-connection/src/relay.rs +++ /dev/null @@ -1,350 +0,0 @@ -use iota_util::route_target::RouteTarget; -use mtp::codec::{ - CommunicationValue, ProtectionPolicy, RelayError, SignaturePolicy, TypeMap, - VerifiedRelayContent, VerifiedRelayMetadata, forward_relay_frame, - open_relay_content_with_keyrings, open_relay_metadata_with, relay_metadata_claimed_signer_id, -}; -use mtp::crypto::{Keyring, PublicKeyBundle}; -use std::fmt; - -pub const RELAY_PROTECTION_POLICY: ProtectionPolicy = ProtectionPolicy { - signature: SignaturePolicy::Dual, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MessageSecurityClass { - RelayOnly, - AuthenticatedPeerControl, - AuthenticatedLocalRequest, -} - -pub fn message_security_class(frame: &CommunicationValue) -> MessageSecurityClass { - const RELAY_ONLY_TYPES: &[mtp::codec::CommunicationType] = &[ - mtp::codec::CommunicationType::MessageSend, - mtp::codec::CommunicationType::MessageLive, - mtp::codec::CommunicationType::MessageState, - mtp::codec::CommunicationType::MessageEdit, - mtp::codec::CommunicationType::MessageEditLive, - mtp::codec::CommunicationType::MessageReactionAdd, - mtp::codec::CommunicationType::MessageReactionRemove, - mtp::codec::CommunicationType::MessageReactionLive, - mtp::codec::CommunicationType::MessageDelete, - mtp::codec::CommunicationType::MessageDeleteLive, - mtp::codec::CommunicationType::MessageOtherIota, - mtp::codec::CommunicationType::SetChatSecret, - mtp::codec::CommunicationType::SendChat, - mtp::codec::CommunicationType::SettingsSave, - mtp::codec::CommunicationType::GlobalSettingsSave, - mtp::codec::CommunicationType::AddConversation, - mtp::codec::CommunicationType::AddCommunity, - mtp::codec::CommunicationType::RemoveCommunity, - ]; - - if RELAY_ONLY_TYPES.iter().any(|kind| frame.is_type(*kind)) { - MessageSecurityClass::RelayOnly - } else if frame.is_type(mtp::codec::CommunicationType::GetChatSecret) - || frame.is_type(mtp::codec::CommunicationType::MessageGet) - || frame.is_type(mtp::codec::CommunicationType::MessagesGet) - { - MessageSecurityClass::AuthenticatedPeerControl - } else { - MessageSecurityClass::AuthenticatedLocalRequest - } -} - -#[derive(Debug, Clone)] -pub struct UserIdentity { - pub user_id: u64, - pub iota_id: u64, - pub signing_keys: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerifiedRelayContext { - pub signer_id: u64, - pub final_recipient_id: u64, - pub message_id: String, - pub created_at: u64, - pub type_map: TypeMap, -} - -#[derive(Debug, Clone)] -pub struct VerifiedRelay { - pub metadata: VerifiedRelayMetadata, - pub context: VerifiedRelayContext, - pub signing_keys: Vec, -} - -#[derive(Debug)] -pub enum RelayValidationError { - WrongNextHop { expected: u64, actual: Option }, - OuterSenderNotAllowed, - MissingSigningKeys(u64), - MissingTypeMap, - InvalidRouteTarget(u64), - KeyLookup(String), - Relay(RelayError), -} - -impl fmt::Display for RelayValidationError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::WrongNextHop { expected, actual } => { - write!( - formatter, - "relay next hop {:?} does not match Iota {expected}", - actual - ) - } - Self::OuterSenderNotAllowed => formatter.write_str("relay has an outer sender"), - Self::MissingSigningKeys(signer_id) => { - write!(formatter, "no trusted signing keys for user {signer_id}") - } - Self::MissingTypeMap => formatter.write_str("relay has no negotiated type map"), - Self::InvalidRouteTarget(target) => { - write!(formatter, "relay has invalid route target {target}") - } - Self::KeyLookup(error) => write!(formatter, "trusted signer lookup failed: {error}"), - Self::Relay(error) => error.fmt(formatter), - } - } -} - -impl std::error::Error for RelayValidationError {} - -impl From for RelayValidationError { - fn from(error: RelayError) -> Self { - Self::Relay(error) - } -} - -/* - * Relay metadata is opened only after the claimed signer selects trusted key - * history. Replay reservation happens after verification and durable - * acceptance, so a failed delivery can be retried without losing the frame. - */ -pub async fn verify_relay_metadata( - frame: &CommunicationValue, - local_iota_id: u64, - keyring: &Keyring, - resolve_signing_keys: F, -) -> Result -where - F: FnOnce(u64) -> Fut, - Fut: Future, RelayValidationError>>, -{ - let expected_next_hop = RouteTarget::Iota(local_iota_id) - .wire_id() - .ok_or(RelayValidationError::InvalidRouteTarget(local_iota_id))?; - if frame.receiver() != Some(expected_next_hop) { - return Err(RelayValidationError::WrongNextHop { - expected: expected_next_hop, - actual: frame.receiver(), - }); - } - if frame.sender().is_some() { - return Err(RelayValidationError::OuterSenderNotAllowed); - } - - let claimed_signer = relay_metadata_claimed_signer_id(frame, &[keyring])?; - let signing_keys = resolve_signing_keys(claimed_signer).await?; - if signing_keys.is_empty() { - return Err(RelayValidationError::MissingSigningKeys(claimed_signer)); - } - - let resolver_keys = signing_keys.clone(); - let type_map = frame - .type_map() - .cloned() - .ok_or(RelayValidationError::MissingTypeMap)?; - let metadata = open_relay_metadata_with( - frame, - &[keyring], - Some(claimed_signer), - move |signer_id| (signer_id == claimed_signer).then(|| resolver_keys.clone()), - RELAY_PROTECTION_POLICY, - None, - )?; - - let context = VerifiedRelayContext { - signer_id: metadata.signer_id(), - final_recipient_id: metadata.final_recipient_id(), - message_id: metadata.message_id().to_owned(), - created_at: metadata.created_at(), - type_map, - }; - - Ok(VerifiedRelay { - metadata, - context, - signing_keys, - }) -} - -pub fn open_verified_relay_content( - relay: &VerifiedRelay, - keyrings: &[&Keyring], - expected_recipient_id: u64, -) -> Result { - Ok(open_relay_content_with_keyrings( - &relay.metadata, - keyrings, - &relay.signing_keys, - Some(expected_recipient_id), - RELAY_PROTECTION_POLICY, - )?) -} - -pub fn forward_verified_relay( - frame: &CommunicationValue, - target: RouteTarget, -) -> Result { - let next_hop_id = target - .wire_id() - .ok_or(RelayValidationError::InvalidRouteTarget(target.id()))?; - Ok(forward_relay_frame(frame, next_hop_id)?) -} - -#[cfg(test)] -mod tests { - use super::*; - use mtp::codec::SealedRelayBuilder; - use mtp::crypto::{DualSigner, Ed25519Signer, Keyring}; - - fn relay(message_id: &str) -> Result<(Keyring, Keyring, CommunicationValue), String> { - let signer_keyring = Keyring::generate(); - let recipient_keyring = Keyring::generate(); - let signer = DualSigner::new( - &signer_keyring.sig_cl_secret_key, - &signer_keyring.sig_pq_secret_key, - &signer_keyring.sig_pq_public_key, - ) - .map_err(|error| error.to_string())?; - let frame = SealedRelayBuilder::new( - "MessageSend", - mtp::codec::DataValue::Str("payload".into()), - 7, - 42, - RouteTarget::Iota(99) - .wire_id() - .ok_or("invalid test target")?, - &signer, - ) - .message_id(message_id) - .created_at(123) - .metadata_recipients(vec![recipient_keyring.public_key_bundle()]) - .content_recipients(vec![recipient_keyring.public_key_bundle()]) - .build() - .map_err(|error| error.to_string())?; - Ok((signer_keyring, recipient_keyring, frame)) - } - - #[tokio::test] - async fn verifies_metadata_with_trusted_signing_key() -> Result<(), String> { - let (signer, recipient, frame) = relay("accepted")?; - let trusted_key = signer.public_key_bundle(); - let verified = verify_relay_metadata(&frame, 99, &recipient, move |signer_id| async move { - (signer_id == 7) - .then_some(vec![trusted_key]) - .ok_or(RelayValidationError::MissingSigningKeys(signer_id)) - }) - .await - .map_err(|error| error.to_string())?; - - assert_eq!(verified.context.signer_id, 7); - assert_eq!(verified.context.final_recipient_id, 42); - assert_eq!(verified.context.message_id, "accepted"); - Ok(()) - } - - #[tokio::test] - async fn rejects_metadata_signed_by_untrusted_key() -> Result<(), String> { - let (_signer, recipient, frame) = relay("wrong-key")?; - let wrong_signer = Keyring::generate(); - let trusted_key = wrong_signer.public_key_bundle(); - let result = verify_relay_metadata(&frame, 99, &recipient, move |_| async move { - Ok(vec![trusted_key]) - }) - .await; - - assert!(matches!(result, Err(RelayValidationError::Relay(_)))); - Ok(()) - } - - #[tokio::test] - async fn rejects_classical_only_relay_under_dual_policy() -> Result<(), String> { - let signer_keyring = Keyring::generate(); - let recipient_keyring = Keyring::generate(); - let signer = Ed25519Signer::new(&signer_keyring.sig_cl_secret_key) - .map_err(|error| error.to_string())?; - let frame = SealedRelayBuilder::new( - "MessageSend", - mtp::codec::DataValue::Str("payload".into()), - 7, - 42, - RouteTarget::Iota(99) - .wire_id() - .ok_or("invalid test target")?, - &signer, - ) - .message_id("classical-only") - .created_at(123) - .metadata_recipients(vec![recipient_keyring.public_key_bundle()]) - .content_recipients(vec![recipient_keyring.public_key_bundle()]) - .build() - .map_err(|error| error.to_string())?; - let trusted_key = signer_keyring.public_key_bundle(); - let result = verify_relay_metadata(&frame, 99, &recipient_keyring, move |_| async move { - Ok(vec![trusted_key]) - }) - .await; - - assert!(matches!(result, Err(RelayValidationError::Relay(_)))); - Ok(()) - } - - #[tokio::test] - async fn rejects_outer_sender_before_key_lookup() -> Result<(), String> { - let (_signer, recipient, frame) = relay("outer-sender")?; - let frame = frame.with_sender(501); - let result = verify_relay_metadata(&frame, 99, &recipient, |_| async { - Err(RelayValidationError::MissingSigningKeys(7)) - }) - .await; - - assert!(matches!( - result, - Err(RelayValidationError::OuterSenderNotAllowed) - )); - Ok(()) - } - - #[tokio::test] - async fn verification_does_not_commit_replay_state() -> Result<(), String> { - let (signer, recipient, frame) = relay("duplicate")?; - let trusted_key = signer.public_key_bundle(); - - for _ in 0..2 { - let trusted_key = trusted_key.clone(); - let result = verify_relay_metadata(&frame, 99, &recipient, move |_| async move { - Ok(vec![trusted_key]) - }) - .await; - let _ = result.map_err(|error| error.to_string())?; - } - Ok(()) - } - - #[test] - fn forwarding_preserves_sealed_payload() -> Result<(), String> { - let (_signer, _recipient, frame) = relay("forwarding")?; - let forwarded = forward_verified_relay(&frame, RouteTarget::User(100)) - .map_err(|error| error.to_string())?; - - assert_eq!(frame.sender(), None); - assert_eq!(forwarded.sender(), None); - assert_eq!(forwarded.receiver(), RouteTarget::User(100).wire_id()); - assert_eq!(frame.payload(), forwarded.payload()); - Ok(()) - } -} diff --git a/iota-daemon-lib/src/command_router.rs b/iota-daemon-lib/src/command_router.rs index 7f9f1c7..74f5f96 100644 --- a/iota-daemon-lib/src/command_router.rs +++ b/iota-daemon-lib/src/command_router.rs @@ -100,25 +100,10 @@ impl CommandRouter { .into_iter() .map(|user| UserSummary { credential_present: user.state == user_manager::LocalUserState::Managed - && user_manager::get_user(user.user_id).is_some_and(|profile| { - iota_util::file_util::read_user_credential_with_legacy( - user.user_id, - &profile.username, - ) - .ok() - .flatten() - .is_some() - }), + && user_manager::get_user(user.user_id).is_some_and(|profile| iota_util::file_util::read_user_credential_with_legacy(user.user_id, &profile.username).ok().flatten().is_some()), user_id: user.user_id, username: user.username, - state: match user.state { - user_manager::LocalUserState::Managed => { - iota_ipc::LocalUserState::Managed - } - user_manager::LocalUserState::Released => { - iota_ipc::LocalUserState::Released - } - }, + state: match user.state { user_manager::LocalUserState::Managed => iota_ipc::LocalUserState::Managed, user_manager::LocalUserState::Released => iota_ipc::LocalUserState::Released }, data_present: user.data_present, }) .collect(); @@ -160,8 +145,7 @@ impl CommandRouter { } } } - LocalRequest::PurgeUserData { user_id } => match user_manager::purge_user_data(user_id) - { + LocalRequest::PurgeUserData { user_id } => match user_manager::purge_user_data(user_id) { Ok(()) => ResponseResult::Ok(ResponsePayload::UserDataPurged { user_id }), Err(error) => { log!("User data purge failed for {user_id}: {error}"); @@ -169,51 +153,25 @@ impl CommandRouter { } }, LocalRequest::AttachUserFromTu { credential } => { - match omikron_connector::user_ops::attach_user_from_tu( - self.services.omikron.as_ref(), - &credential.0, - ) - .await - { - Ok(user) => ResponseResult::Ok(ResponsePayload::Acknowledged { - message: format!("Added {} ({}) to this Iota", user.username, user.user_id), - }), + match omikron_connector::user_ops::attach_user_from_tu(self.services.omikron.as_ref(), &credential.0).await { + Ok(user) => ResponseResult::Ok(ResponsePayload::Acknowledged { message: format!("Added {} ({}) to this Iota", user.username, user.user_id) }), Err(error) => { log!("Credential attach failed: {error:?}"); ResponseResult::Error(IpcErrorCode::Unauthorized) } } } - LocalRequest::CompleteDeleteUser { - user_id, - credential, - } => { + LocalRequest::CompleteDeleteUser { user_id, credential } => { let contents = match credential { Some(value) => Ok(value.0), None => user_manager::get_user(user_id) .ok_or(()) - .and_then(|user| { - iota_util::file_util::read_user_credential_with_legacy( - user_id, - &user.username, - ) - .map_err(|_| ()) - }) + .and_then(|user| iota_util::file_util::read_user_credential_with_legacy(user_id, &user.username).map_err(|_| ())) .and_then(|value| value.ok_or(())), }; - let Ok(contents) = contents else { - return ResponseResult::Error(IpcErrorCode::Unauthorized); - }; - match omikron_connector::user_ops::complete_delete_user_with_tu( - self.services.omikron.as_ref(), - &contents, - user_id, - ) - .await - { - Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { - message: format!("Deleted Tensamin account {user_id}"), - }), + let Ok(contents) = contents else { return ResponseResult::Error(IpcErrorCode::Unauthorized); }; + match omikron_connector::user_ops::complete_delete_user_with_tu(self.services.omikron.as_ref(), &contents, user_id).await { + Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { message: format!("Deleted Tensamin account {user_id}") }), Err(error) => { log!("Credential deletion failed for {user_id}: {error:?}"); ResponseResult::Error(IpcErrorCode::Unauthorized) @@ -227,34 +185,19 @@ impl CommandRouter { } let request = CommunicationValue::new(CommunicationType::ReleaseUserFromIota) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())); - match self - .services - .omikron - .await_response(&request, Duration::from_secs(20)) - .await - { - Ok(response) if response.is_type(CommunicationType::Success) => { - match user_manager::release_user(user_id) { - Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { - message: format!( - "Released user {user_id}; hosted data was retained" - ), - }), - Err(error) => { - log!( - "Remote release succeeded but local cleanup failed for {user_id}: {error}" - ); - ResponseResult::Error(IpcErrorCode::StorageFailure) - } + match self.services.omikron.await_response(&request, Duration::from_secs(20)).await { + Ok(response) if response.is_type(CommunicationType::Success) => match user_manager::release_user(user_id) { + Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { + message: format!("Released user {user_id}; hosted data was retained"), + }), + Err(error) => { + log!("Remote release succeeded but local cleanup failed for {user_id}: {error}"); + ResponseResult::Error(IpcErrorCode::StorageFailure) } - } - Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => { - ResponseResult::Error(IpcErrorCode::Unauthorized) - } + }, + Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => ResponseResult::Error(IpcErrorCode::Unauthorized), Ok(_) => ResponseResult::Error(IpcErrorCode::Conflict), - Err(omikron_connector::OmikronError::Timeout(_)) => { - ResponseResult::Error(IpcErrorCode::Timeout) - } + Err(omikron_connector::OmikronError::Timeout(_)) => ResponseResult::Error(IpcErrorCode::Timeout), Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable), } } @@ -351,28 +294,18 @@ impl CommandRouter { } LocalRequest::GetUser { user_id } => match user_manager::get_user(user_id) { Some(user) => { - let credential_present = - iota_util::file_util::read_user_credential_with_legacy( - user_id, - &user.username, - ) - .ok() - .flatten() - .is_some(); + let credential_present = iota_util::file_util::read_user_credential_with_legacy(user_id, &user.username).ok().flatten().is_some(); ResponseResult::Ok(ResponsePayload::UserDetail(UserDetailResponse { - user_id: user.user_id, - username: user.username, - display_name: user.display_name, - created_at: user.created_at, - trusted_apps: user.trusted_apps.keys().cloned().collect(), - state: iota_ipc::LocalUserState::Managed, - data_present: user_manager::get_residency() - .iter() - .find(|entry| entry.user_id == user_id) - .is_none_or(|entry| entry.data_present), - credential_present, - })) - } + user_id: user.user_id, + username: user.username, + display_name: user.display_name, + created_at: user.created_at, + trusted_apps: user.trusted_apps.keys().cloned().collect(), + state: iota_ipc::LocalUserState::Managed, + data_present: user_manager::get_residency().iter().find(|entry| entry.user_id == user_id).is_none_or(|entry| entry.data_present), + credential_present, + })) + }, None => ResponseResult::Error(IpcErrorCode::NotFound), }, LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), diff --git a/iota-daemon/src/main.rs b/iota-daemon/src/main.rs index cfe0337..76eeaf0 100644 --- a/iota-daemon/src/main.rs +++ b/iota-daemon/src/main.rs @@ -215,14 +215,8 @@ async fn main() -> ExitCode { .spawn_tracked("user-lifecycle-reconciliation", async move { let mut states = omikron_reconcile.connection_state(); loop { - if matches!( - *states.borrow(), - omikron_connector::omikron_connection::ConnectionState::Connected { .. } - ) { - omikron_connector::user_ops::reconcile_managed_users( - omikron_reconcile.as_ref(), - ) - .await; + if matches!(*states.borrow(), omikron_connector::omikron_connection::ConnectionState::Connected { .. }) { + omikron_connector::user_ops::reconcile_managed_users(omikron_reconcile.as_ref()).await; } tokio::select! { changed = states.changed() => if changed.is_err() { break }, diff --git a/iota-ipc/src/lib.rs b/iota-ipc/src/lib.rs index af695f2..bd3d102 100644 --- a/iota-ipc/src/lib.rs +++ b/iota-ipc/src/lib.rs @@ -6,10 +6,9 @@ pub use protocol::{ ClientMessage, CommunitySummary, ComponentHealth, ComponentId, ComponentStatusResponse, ConfigResponse, ConnectionStatus, DaemonMessage, DaemonStatusResponse, DeploymentMode, ExitIntent, HealthStatus, HelloAck, IpcErrorCode, LifecycleEvent, LifecyclePhase, LocalRequest, - LocalUserState, LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse, - RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult, SecretString, StartupPhase, - StateSnapshot, StatusResponse, SupervisorKind, TaskSummary, UpdateStatusResponse, - UserDetailResponse, UserSummary, + LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse, RequestEnvelope, + ResponseEnvelope, ResponsePayload, ResponseResult, StartupPhase, StateSnapshot, StatusResponse, + SecretString, SupervisorKind, TaskSummary, UpdateStatusResponse, UserDetailResponse, UserSummary, LocalUserState, }; pub use transport::{read_msg, write_msg}; diff --git a/iota-ipc/src/protocol.rs b/iota-ipc/src/protocol.rs index 01cd14e..2e2be1a 100644 --- a/iota-ipc/src/protocol.rs +++ b/iota-ipc/src/protocol.rs @@ -163,20 +163,11 @@ pub enum ResponsePayload { Status(StatusResponse), Tasks(Vec), Users(Vec), - UserCreated { - user_id: i64, - username: String, - }, + UserCreated { user_id: i64, username: String }, /// Retained only for wire compatibility. New lifecycle code never emits it. - UserRemoved { - user_id: i64, - }, - UserDataPurged { - user_id: i64, - }, - Acknowledged { - message: String, - }, + UserRemoved { user_id: i64 }, + UserDataPurged { user_id: i64 }, + Acknowledged { message: String }, DaemonStatus(DaemonStatusResponse), Config(ConfigResponse), OmikronStatus(OmikronStatusResponse), diff --git a/iota-logger/src/lib.rs b/iota-logger/src/lib.rs index d7d336a..fe3ed8e 100644 --- a/iota-logger/src/lib.rs +++ b/iota-logger/src/lib.rs @@ -318,29 +318,28 @@ pub fn log_cv_internal( pub fn format_cv(cv: &CommunicationValue) -> String { let mut parts = Vec::new(); - match (cv.sender(), cv.receiver()) { - (Some(sender), Some(receiver)) => parts.push(format!("{} > {}", sender, receiver)), - (Some(sender), None) => parts.push(sender.to_string()), - (None, Some(receiver)) => parts.push(format!("> {}", receiver)), - (None, None) => {} + let sender = cv.get_sender(); + let receiver = cv.get_receiver(); + + if sender > 0 && receiver > 0 { + parts.push(format!("{} > {}", sender, receiver)); + } else if sender > 0 { + parts.push(format!("{}", sender)); + } else if receiver > 0 { + parts.push(format!("> {}", receiver)); } let comm_type = cv .get_comm_type_enum() .map(|kind| kind.to_string()) .unwrap_or_else(|| cv.get_type().to_string()); - let id = cv - .id() - .map_or_else(|| "none".to_string(), |value| value.to_string()); - parts.push(format!("{} (id={})", comm_type, id)); + parts.push(format!("{} (id={})", comm_type, cv.get_id())); - let version = cv - .type_map() - .map(|type_map| type_map.version.clone()) - .unwrap_or_else(|| Version(3, 0)); - let formated_data = cv.data().map_or_else( - || "".to_string(), - |data| format_data_container(data.to_vec(), version), + let data = cv.data(); + + let formated_data = format_data_container( + data.iter().map(|(k, v)| (*k, v.clone())).collect(), + Version(1, 0), ); parts.push(format!("{}", formated_data)); diff --git a/iota-storage/src/users/user_manager.rs b/iota-storage/src/users/user_manager.rs index ed2294f..ccd3c9e 100644 --- a/iota-storage/src/users/user_manager.rs +++ b/iota-storage/src/users/user_manager.rs @@ -19,10 +19,7 @@ pub struct UserResidency { } fn now_millis() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64 + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as i64 } pub fn add_user(user: UserProfile) { @@ -238,10 +235,7 @@ pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageErr })?; db::with_db(|conn| { let tx = conn.unchecked_transaction()?; - tx.execute( - "DELETE FROM trusted_apps WHERE user_id = ?1", - params![user_id], - )?; + tx.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?; tx.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?; tx.execute( r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at) @@ -252,8 +246,7 @@ pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageErr tx.commit()?; Ok(()) })?; - remove_user_credential(user_id) - .map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) + remove_user_credential(user_id).map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) } /// Authoritative hosted-data erasure used by local purge and future Omega @@ -263,35 +256,14 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage let tx = conn.unchecked_transaction()?; tx.execute("DELETE FROM message_edits WHERE message_id IN (SELECT id FROM messages WHERE storage_owner = ?1)", params![user_id])?; tx.execute("DELETE FROM reactions WHERE message_id IN (SELECT id FROM messages WHERE storage_owner = ?1)", params![user_id])?; - tx.execute( - "DELETE FROM messages WHERE storage_owner = ?1", - params![user_id], - )?; - tx.execute( - "DELETE FROM contacts WHERE storage_owner = ?1", - params![user_id], - )?; - tx.execute( - "DELETE FROM communities WHERE storage_owner = ?1", - params![user_id], - )?; + tx.execute("DELETE FROM messages WHERE storage_owner = ?1", params![user_id])?; + tx.execute("DELETE FROM contacts WHERE storage_owner = ?1", params![user_id])?; + tx.execute("DELETE FROM communities WHERE storage_owner = ?1", params![user_id])?; tx.execute("DELETE FROM settings WHERE user_id = ?1", params![user_id])?; - tx.execute( - "DELETE FROM sync_events WHERE user_id = ?1", - params![user_id], - )?; - tx.execute( - "DELETE FROM sync_heads WHERE user_id = ?1", - params![user_id], - )?; - tx.execute( - "DELETE FROM client_sync_state WHERE user_id = ?1", - params![user_id], - )?; - tx.execute( - "DELETE FROM trusted_apps WHERE user_id = ?1", - params![user_id], - )?; + tx.execute("DELETE FROM sync_events WHERE user_id = ?1", params![user_id])?; + tx.execute("DELETE FROM sync_heads WHERE user_id = ?1", params![user_id])?; + tx.execute("DELETE FROM client_sync_state WHERE user_id = ?1", params![user_id])?; + tx.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?; tx.execute( "UPDATE user_residency SET data_state = 'empty', updated_at = ?2 WHERE user_id = ?1", params![user_id, now_millis()], @@ -301,8 +273,7 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage })?; crate::util::e2ee_storage::purge_user(user_id) .map_err(crate::storage_error::StorageError::Other)?; - delete_user_directory(user_id) - .map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) + delete_user_directory(user_id).map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) } /// Complete local erasure is idempotent and is the target for a durable @@ -310,19 +281,12 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::StorageError> { purge_user_data(user_id)?; db::with_db(|conn| { - conn.execute( - "DELETE FROM trusted_apps WHERE user_id = ?1", - params![user_id], - )?; + conn.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?; conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?; - conn.execute( - "DELETE FROM user_residency WHERE user_id = ?1", - params![user_id], - )?; + conn.execute("DELETE FROM user_residency WHERE user_id = ?1", params![user_id])?; Ok(()) })?; - remove_user_credential(user_id) - .map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) + remove_user_credential(user_id).map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) } pub fn get_residency() -> Vec { @@ -342,15 +306,14 @@ pub fn get_residency() -> Vec { pub fn clear() { if let Err(e) = db::with_db(|conn| { - conn.execute_batch( - "DELETE FROM trusted_apps; DELETE FROM users; DELETE FROM user_residency;", - )?; + conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users; DELETE FROM user_residency;")?; Ok(()) }) { eprintln!("Failed to clear users: {}", e); } } + pub fn save_users() { // No-op: users are auto-saved via SQLite. } diff --git a/iota-storage/src/util/db.rs b/iota-storage/src/util/db.rs index 01ae3a5..82968e7 100644 --- a/iota-storage/src/util/db.rs +++ b/iota-storage/src/util/db.rs @@ -77,22 +77,12 @@ fn add_column_if_missing( column: &str, definition: &str, ) -> Result<(), StorageError> { - add_table_column_if_missing(conn, "messages", column, definition) -} - -fn add_table_column_if_missing( - conn: &Connection, - table: &str, - column: &str, - definition: &str, -) -> Result<(), StorageError> { - let mut statement = conn.prepare(&format!( - "SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?1" - ))?; + let mut statement = + conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; let exists = statement.exists([column])?; if !exists { - conn.execute_batch(&format!("ALTER TABLE {table} ADD COLUMN {definition};"))?; + conn.execute_batch(&format!("ALTER TABLE messages ADD COLUMN {definition};"))?; } Ok(()) @@ -276,72 +266,6 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> { )?; } - if current_version < 8 { - conn.execute_batch( - r#" - CREATE TABLE IF NOT EXISTS relay_replay ( - signer_id INTEGER NOT NULL, - message_id TEXT NOT NULL, - created_at INTEGER NOT NULL, - PRIMARY KEY (signer_id, message_id) - ); - CREATE INDEX IF NOT EXISTS idx_relay_replay_created_at - ON relay_replay (created_at); - CREATE TABLE IF NOT EXISTS pending_relays ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - destination_id INTEGER NOT NULL, - target_kind INTEGER NOT NULL DEFAULT 0, - frame BLOB NOT NULL, - created_at INTEGER NOT NULL, - frame_id INTEGER NOT NULL DEFAULT 0, - UNIQUE(destination_id, frame) - ); - CREATE INDEX IF NOT EXISTS idx_pending_relays_destination - ON pending_relays (destination_id, id); - PRAGMA user_version = 8; - "#, - )?; - } - - if current_version < 9 { - add_table_column_if_missing( - conn, - "pending_relays", - "target_kind", - "target_kind INTEGER NOT NULL DEFAULT 0", - )?; - add_table_column_if_missing( - conn, - "pending_relays", - "type_map_version", - "type_map_version TEXT NOT NULL DEFAULT '1.0'", - )?; - add_table_column_if_missing( - conn, - "pending_relays", - "frame_id", - "frame_id INTEGER NOT NULL DEFAULT 0", - )?; - conn.execute_batch( - r#" - CREATE TABLE IF NOT EXISTS relay_inbox ( - signer_id INTEGER NOT NULL, - message_id TEXT NOT NULL, - created_at INTEGER NOT NULL, - destination_id INTEGER NOT NULL, - frame BLOB NOT NULL, - type_map_version TEXT NOT NULL, - frame_id INTEGER NOT NULL, - state TEXT NOT NULL CHECK (state IN ('received', 'applied', 'queued', 'delivered', 'rejected')), - PRIMARY KEY (signer_id, message_id) - ); - CREATE INDEX IF NOT EXISTS idx_relay_inbox_state - ON relay_inbox (state, created_at); - PRAGMA user_version = 9; - "#, - )?; - } - Ok(()) } @@ -411,7 +335,7 @@ mod tests { run_migrations_on_connection(&conn)?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; - assert_eq!(version, 9); + assert_eq!(version, 7); for column in ["height", "reply_to", "edited_count", "deleted_by_external"] { let mut statement = conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; @@ -428,16 +352,8 @@ mod tests { run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; - assert_eq!(version, 9); - for table in [ - "sync_heads", - "sync_events", - "client_sync_state", - "user_residency", - "relay_replay", - "pending_relays", - "relay_inbox", - ] { + assert_eq!(version, 7); + for table in ["sync_heads", "sync_events", "client_sync_state", "user_residency"] { let exists: i64 = conn.query_row( "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1", [table], @@ -445,11 +361,6 @@ mod tests { )?; assert_eq!(exists, 1); } - for column in ["frame_id", "target_kind", "type_map_version"] { - let mut statement = - conn.prepare("SELECT 1 FROM pragma_table_info('pending_relays') WHERE name = ?1")?; - assert!(statement.exists([column])?); - } Ok(()) } } diff --git a/iota-storage/src/util/e2ee_storage.rs b/iota-storage/src/util/e2ee_storage.rs index 55cf8cd..6b13018 100644 --- a/iota-storage/src/util/e2ee_storage.rs +++ b/iota-storage/src/util/e2ee_storage.rs @@ -24,6 +24,19 @@ pub struct ChatSecretQuery { pub secret_id: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PendingChatSecretForward { + pub recipient_user_id: String, + pub chat_id: String, + pub sender_user_id: String, + pub secret_id: String, + pub version: i64, + pub encrypted_secret: Vec, + pub kem_ciphertext: Vec, + pub wrapping_scheme: String, + pub created_at: i64, +} + static E2EE_DB: LazyLock>> = LazyLock::new(|| { db::create_shared_connection( "e2ee", @@ -50,6 +63,21 @@ static E2EE_DB: LazyLock>> = LazyLock::new(|| { CREATE INDEX IF NOT EXISTS idx_chat_secrets_owner ON chat_secrets (user_id, chat_id, secret_id); + CREATE TABLE IF NOT EXISTS pending_chat_secret_forwards ( + recipient_user_id TEXT NOT NULL, + chat_id TEXT NOT NULL, + sender_user_id TEXT NOT NULL, + secret_id TEXT NOT NULL, + version INTEGER NOT NULL, + encrypted_secret BLOB NOT NULL, + kem_ciphertext BLOB NOT NULL, + wrapping_scheme TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (recipient_user_id, chat_id, secret_id) + ); + + CREATE INDEX IF NOT EXISTS idx_pending_chat_secret_forwards_recipient + ON pending_chat_secret_forwards (recipient_user_id, created_at); "#, ) .expect("Failed to create or initialize E2EE DB") @@ -87,15 +115,89 @@ pub fn put_chat_secret(record: StoredChatSecret) -> Result<(), StorageError> { }) } -/// Erase every E2EE record owned by a user. The operation is +pub fn put_pending_chat_secret_forward( + record: PendingChatSecretForward, +) -> Result<(), StorageError> { + db::with_conn(&E2EE_DB, |conn| { + conn.execute( + r#" + INSERT INTO pending_chat_secret_forwards ( + recipient_user_id, chat_id, sender_user_id, secret_id, version, + encrypted_secret, kem_ciphertext, wrapping_scheme, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) + ON CONFLICT(recipient_user_id, chat_id, secret_id) DO UPDATE SET + sender_user_id = excluded.sender_user_id, + version = excluded.version, + encrypted_secret = excluded.encrypted_secret, + kem_ciphertext = excluded.kem_ciphertext, + wrapping_scheme = excluded.wrapping_scheme, + created_at = excluded.created_at + "#, + params![ + record.recipient_user_id, + record.chat_id, + record.sender_user_id, + record.secret_id, + record.version, + record.encrypted_secret, + record.kem_ciphertext, + record.wrapping_scheme, + record.created_at, + ], + )?; + Ok(()) + }) +} + +pub fn get_pending_chat_secret_forwards( + limit: i64, +) -> Result, StorageError> { + db::with_conn(&E2EE_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT recipient_user_id, chat_id, sender_user_id, secret_id, version, + encrypted_secret, kem_ciphertext, wrapping_scheme, created_at + FROM pending_chat_secret_forwards + ORDER BY created_at ASC + LIMIT ?1 + "#, + )?; + let rows = stmt.query_map(params![limit.clamp(1, 500)], pending_forward_from_row)?; + let mut out = Vec::new(); + for row in rows { + out.push(row?); + } + Ok(out) + }) +} + +pub fn delete_pending_chat_secret_forward( + recipient_user_id: &str, + chat_id: &str, + secret_id: &str, +) -> Result<(), StorageError> { + db::with_conn(&E2EE_DB, |conn| { + conn.execute( + r#" + DELETE FROM pending_chat_secret_forwards + WHERE recipient_user_id = ?1 AND chat_id = ?2 AND secret_id = ?3 + "#, + params![recipient_user_id, chat_id, secret_id], + )?; + Ok(()) + }) +} + +/// Erase every E2EE record owned by, or queued for, a user. The operation is /// intentionally idempotent so it can be retried after an interrupted remote /// erasure request. pub fn purge_user(user_id: i64) -> Result<(), StorageError> { let user_id = user_id.to_string(); db::with_conn(&E2EE_DB, |conn| { let tx = conn.unchecked_transaction()?; + tx.execute("DELETE FROM chat_secrets WHERE user_id = ?1", params![user_id])?; tx.execute( - "DELETE FROM chat_secrets WHERE user_id = ?1", + "DELETE FROM pending_chat_secret_forwards WHERE recipient_user_id = ?1 OR sender_user_id = ?1", params![user_id], )?; tx.commit()?; @@ -140,3 +242,17 @@ fn chat_secret_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result) -> rusqlite::Result { + Ok(PendingChatSecretForward { + recipient_user_id: row.get(0)?, + chat_id: row.get(1)?, + sender_user_id: row.get(2)?, + secret_id: row.get(3)?, + version: row.get(4)?, + encrypted_secret: row.get(5)?, + kem_ciphertext: row.get(6)?, + wrapping_scheme: row.get(7)?, + created_at: row.get(8)?, + }) +} diff --git a/iota-storage/src/util/mod.rs b/iota-storage/src/util/mod.rs index 2a1eec6..7412c7b 100644 --- a/iota-storage/src/util/mod.rs +++ b/iota-storage/src/util/mod.rs @@ -4,7 +4,5 @@ pub mod communities_util; pub mod config_util; pub mod db; pub mod e2ee_storage; -pub mod relay_queue; -pub mod relay_replay; pub mod settings; pub mod sync; diff --git a/iota-storage/src/util/relay_queue.rs b/iota-storage/src/util/relay_queue.rs deleted file mode 100644 index 835ebcd..0000000 --- a/iota-storage/src/util/relay_queue.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::storage_error::StorageError; -use crate::util::db; -use iota_util::route_target::RouteTarget; -use rusqlite::params; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PendingRelay { - pub id: i64, - pub target: RouteTarget, - pub frame: Vec, - pub created_at: i64, - pub frame_id: u32, - pub type_map_version: String, -} - -pub fn enqueue( - target: RouteTarget, - frame: &[u8], - created_at: i64, - frame_id: u32, - type_map_version: &str, -) -> Result<(), StorageError> { - let destination_id = i64::try_from(target.id()) - .map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?; - let target_kind = match target { - RouteTarget::User(_) => 0_i64, - RouteTarget::Iota(_) => 1_i64, - }; - db::with_db(|connection| { - connection.execute( - "INSERT OR IGNORE INTO pending_relays (destination_id, target_kind, frame, created_at, frame_id, type_map_version) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![ - destination_id, - target_kind, - frame, - created_at, - i64::from(frame_id), - type_map_version - ], - )?; - Ok(()) - }) -} - -pub fn list(limit: i64) -> Result, StorageError> { - db::with_db(|connection| { - let mut statement = connection.prepare( - "SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version FROM pending_relays ORDER BY id LIMIT ?1", - )?; - let rows = statement.query_map(params![limit.clamp(1, 500)], |row| { - let destination_id = row.get::<_, i64>(1)?; - let target_kind = row.get::<_, i64>(2)?; - let destination_id = u64::try_from(destination_id).map_err(|_| { - rusqlite::Error::FromSqlConversionFailure( - 1, - rusqlite::types::Type::Integer, - "negative relay destination ID".into(), - ) - })?; - let target = match target_kind { - 0 => RouteTarget::User(destination_id), - 1 => RouteTarget::Iota(destination_id), - _ => { - return Err(rusqlite::Error::FromSqlConversionFailure( - 2, - rusqlite::types::Type::Integer, - "invalid relay target kind".into(), - )); - } - }; - Ok(PendingRelay { - id: row.get(0)?, - target, - frame: row.get(3)?, - created_at: row.get(4)?, - frame_id: u32::try_from(row.get::<_, i64>(5)?).map_err(|_| { - rusqlite::Error::FromSqlConversionFailure( - 5, - rusqlite::types::Type::Integer, - "negative relay frame ID".into(), - ) - })?, - type_map_version: row.get(6)?, - }) - })?; - rows.collect::, _>>().map_err(Into::into) - }) -} - -pub fn acknowledge(destination_id: u64, frame_id: u32) -> Result { - let destination_id = i64::try_from(destination_id) - .map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?; - db::with_db(|connection| { - let changed = connection.execute( - "DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = 0 AND frame_id = ?2", - params![destination_id, i64::from(frame_id)], - )?; - Ok(changed == 1) - }) -} - -pub fn acknowledge_iota(destination_id: u64, frame_id: u32) -> Result { - let destination_id = i64::try_from(destination_id) - .map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?; - db::with_db(|connection| { - let changed = connection.execute( - "DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", - params![destination_id, i64::from(frame_id)], - )?; - Ok(changed == 1) - }) -} - -pub fn remove_for_frame(target: RouteTarget, frame_id: u32) -> Result { - let destination_id = i64::try_from(target.id()) - .map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?; - let target_kind = match target { - RouteTarget::User(_) => 0_i64, - RouteTarget::Iota(_) => 1_i64, - }; - db::with_db(|connection| { - let changed = connection.execute( - "DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = ?2 AND frame_id = ?3", - params![destination_id, target_kind, i64::from(frame_id)], - )?; - Ok(changed == 1) - }) -} - -pub fn delete(id: i64) -> Result<(), StorageError> { - db::with_db(|connection| { - connection.execute("DELETE FROM pending_relays WHERE id = ?1", params![id])?; - Ok(()) - }) -} diff --git a/iota-storage/src/util/relay_replay.rs b/iota-storage/src/util/relay_replay.rs deleted file mode 100644 index bc3bda7..0000000 --- a/iota-storage/src/util/relay_replay.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::storage_error::StorageError; -use crate::util::db; -use rusqlite::params; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RelayReservation { - New, - Existing { state: String, frame_matches: bool }, -} - -pub fn reserve( - signer_id: u64, - message_id: &str, - created_at: u64, - destination_id: u64, - frame: &[u8], - frame_id: u32, - type_map_version: &str, -) -> Result { - let signer_id = i64::try_from(signer_id) - .map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?; - let created_at = i64::try_from(created_at) - .map_err(|_| StorageError::Other("relay creation time exceeds SQLite range".into()))?; - let destination_id = i64::try_from(destination_id) - .map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?; - - db::with_db(|connection| { - let inserted = connection.execute( - "INSERT OR IGNORE INTO relay_inbox (signer_id, message_id, created_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'received')", - params![ - signer_id, - message_id, - created_at, - destination_id, - frame, - i64::from(frame_id), - type_map_version - ], - )?; - if inserted == 1 { - return Ok(RelayReservation::New); - } - - let (state, existing_destination_id, existing_frame, existing_type_map_version): - (String, i64, Vec, String) = connection.query_row( - "SELECT state, destination_id, frame, type_map_version FROM relay_inbox WHERE signer_id = ?1 AND message_id = ?2", - params![signer_id, message_id], - |row| { - Ok(( - row.get(0)?, - row.get(1)?, - row.get::<_, Vec>(2)?, - row.get(3)?, - )) - }, - )?; - Ok(RelayReservation::Existing { - state, - frame_matches: existing_destination_id == destination_id - && existing_frame == frame - && existing_type_map_version == type_map_version, - }) - }) -} - -pub fn mark_delivered_for_frame(destination_id: u64, frame_id: u32) -> Result<(), StorageError> { - let destination_id = i64::try_from(destination_id) - .map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?; - db::with_db(|connection| { - connection.execute( - "UPDATE relay_inbox SET state = 'delivered' WHERE destination_id = ?1 AND frame_id = ?2", - params![destination_id, i64::from(frame_id)], - )?; - Ok(()) - }) -} - -pub fn mark_state(signer_id: u64, message_id: &str, state: &str) -> Result<(), StorageError> { - if !matches!( - state, - "received" | "applied" | "queued" | "delivered" | "rejected" - ) { - return Err(StorageError::Other("invalid relay inbox state".into())); - } - let signer_id = i64::try_from(signer_id) - .map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?; - db::with_db(|connection| { - connection.execute( - "UPDATE relay_inbox SET state = ?3 WHERE signer_id = ?1 AND message_id = ?2", - params![signer_id, message_id, state], - )?; - Ok(()) - }) -} - -pub fn prune_completed(before_created_at: i64) -> Result<(), StorageError> { - db::with_db(|connection| { - connection.execute( - "DELETE FROM relay_inbox WHERE created_at < ?1 AND state IN ('delivered', 'rejected')", - params![before_created_at], - )?; - connection.execute( - "DELETE FROM relay_replay WHERE created_at < ?1", - params![before_created_at], - )?; - Ok(()) - }) -} - -pub fn accept(signer_id: u64, message_id: &str, created_at: u64) -> Result { - let signer_id = i64::try_from(signer_id) - .map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?; - let created_at = i64::try_from(created_at) - .map_err(|_| StorageError::Other("relay creation time exceeds SQLite range".into()))?; - - db::with_db(|connection| { - let inserted = connection.execute( - "INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)", - params![signer_id, message_id, created_at], - )?; - Ok(inserted == 1) - }) -} - -#[cfg(test)] -mod tests { - use rusqlite::{Connection, params}; - - #[test] - fn replay_identity_uses_signer_and_message_id() -> Result<(), rusqlite::Error> { - let connection = Connection::open_in_memory()?; - connection.execute_batch( - "CREATE TABLE relay_replay (signer_id INTEGER NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (signer_id, message_id));", - )?; - - let first = connection.execute( - "INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)", - params![7_i64, "message", 1_i64], - )?; - let duplicate = connection.execute( - "INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)", - params![7_i64, "message", 2_i64], - )?; - let other_signer = connection.execute( - "INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)", - params![8_i64, "message", 2_i64], - )?; - - assert_eq!((first, duplicate, other_signer), (1, 0, 1)); - Ok(()) - } -} diff --git a/iota-util/Cargo.toml b/iota-util/Cargo.toml index 49bfe51..e9907eb 100644 --- a/iota-util/Cargo.toml +++ b/iota-util/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] iota-paths = { path = "../iota-paths" } -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ "crypto" ] } diff --git a/iota-util/src/crypto_util.rs b/iota-util/src/crypto_util.rs index bbb884d..64056d9 100644 --- a/iota-util/src/crypto_util.rs +++ b/iota-util/src/crypto_util.rs @@ -1,50 +1,5 @@ use base64::{Engine as _, engine::general_purpose::STANDARD}; -use mtp::crypto::{ - EncryptionType, Keyring, MultiEncryptedMessage, PublicKeyBundle, decrypt_multi_for, - encrypt_multi_for, -}; - -const CHALLENGE_PURPOSE: u8 = 0x01; -const LEGACY_AAD_DOMAIN: &[u8] = b"IOTA-MTP-AAD-1"; - -fn bind_aad(plaintext: &[u8], aad: &[u8]) -> Result, String> { - let aad_len = u32::try_from(aad.len()) - .map_err(|_| "associated data is too large to encode".to_string())?; - let mut bound = Vec::with_capacity( - LEGACY_AAD_DOMAIN - .len() - .saturating_add(4) - .saturating_add(aad.len()) - .saturating_add(plaintext.len()), - ); - bound.extend_from_slice(LEGACY_AAD_DOMAIN); - bound.extend_from_slice(&aad_len.to_be_bytes()); - bound.extend_from_slice(aad); - bound.extend_from_slice(plaintext); - Ok(bound) -} - -fn unbind_aad(bound: &[u8], aad: &[u8]) -> Result, String> { - let header_len = LEGACY_AAD_DOMAIN.len() + 4; - if bound.len() < header_len || &bound[..LEGACY_AAD_DOMAIN.len()] != LEGACY_AAD_DOMAIN { - return Err("associated-data binding is invalid".to_string()); - } - let length_start = LEGACY_AAD_DOMAIN.len(); - let length_end = length_start + 4; - let aad_len = u32::from_be_bytes( - bound[length_start..length_end] - .try_into() - .map_err(|_| "associated-data length is invalid".to_string())?, - ) as usize; - let aad_start = length_end; - let aad_end = aad_start - .checked_add(aad_len) - .ok_or_else(|| "associated-data length overflows".to_string())?; - if aad_end > bound.len() || &bound[aad_start..aad_end] != aad { - return Err("associated data does not match".to_string()); - } - Ok(bound[aad_end..].to_vec()) -} +use mtp::crypto::{EncryptionType, Keyring, PublicKeyBundle, decrypt_with, encrypt_for}; #[derive(Clone, Copy, Debug)] pub enum DataFormat { @@ -58,25 +13,17 @@ pub fn encrypt( aad: &[u8], recipient_pub_key_bundle: &PublicKeyBundle, ) -> Result, String> { - let bound_plaintext = bind_aad(plaintext, aad)?; - let encrypted = encrypt_multi_for( + encrypt_for( EncryptionType::MlKemChaCha20Poly1305, - CHALLENGE_PURPOSE, - &bound_plaintext, - std::slice::from_ref(recipient_pub_key_bundle), + recipient_pub_key_bundle, + plaintext, + aad, ) - .map_err(|e| format!("encryption error: {e:?}"))?; - encrypted - .to_bytes() - .map_err(|e| format!("encryption encoding error: {e:?}")) + .map_err(|e| format!("encryption error: {:?}", e)) } pub fn decrypt(ciphertext: &[u8], aad: &[u8], keyring: &Keyring) -> Result, String> { - let message = MultiEncryptedMessage::from_bytes(ciphertext) - .map_err(|e| format!("decryption envelope error: {e:?}"))?; - let bound_plaintext = decrypt_multi_for(&message, CHALLENGE_PURPOSE, keyring) - .map_err(|e| format!("decryption error: {e:?}"))?; - unbind_aad(&bound_plaintext, aad) + decrypt_with(ciphertext, keyring, aad).map_err(|e| format!("decryption error: {:?}", e)) } pub fn encrypt_challenge( @@ -104,18 +51,3 @@ pub fn export(data: &[u8], format: DataFormat) -> Result { DataFormat::Hex => Ok(hex::encode(data)), } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn encrypt_decrypt_binds_associated_data() -> Result<(), String> { - let keyring = Keyring::generate(); - let ciphertext = encrypt(b"challenge", b"context", &keyring.public_key_bundle())?; - - assert_eq!(decrypt(&ciphertext, b"context", &keyring)?, b"challenge"); - assert!(decrypt(&ciphertext, b"other-context", &keyring).is_err()); - Ok(()) - } -} diff --git a/iota-util/src/file_util.rs b/iota-util/src/file_util.rs index b70f7cc..8412b10 100755 --- a/iota-util/src/file_util.rs +++ b/iota-util/src/file_util.rs @@ -44,9 +44,7 @@ pub fn delete_user_directory(user_id: i64) -> io::Result<()> { } pub fn credential_path(user_id: i64) -> PathBuf { - storage_directory() - .join("credentials") - .join(format!("{user_id}.tu")) + storage_directory().join("credentials").join(format!("{user_id}.tu")) } pub fn read_user_credential(user_id: i64) -> io::Result> { @@ -60,10 +58,7 @@ pub fn read_user_credential(user_id: i64) -> io::Result> { /// Resolve a credential by immutable account id. A valid legacy /// `.tu` is migrated atomically the first time it is encountered. -pub fn read_user_credential_with_legacy( - user_id: i64, - username: &str, -) -> io::Result> { +pub fn read_user_credential_with_legacy(user_id: i64, username: &str) -> io::Result> { if let Some(credential) = read_user_credential(user_id)? { return Ok(Some(credential)); } @@ -76,10 +71,7 @@ pub fn read_user_credential_with_legacy( let parsed = crate::tu::TuCredential::parse(&credential) .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; if parsed.user_id != user_id { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "legacy credential user id mismatch", - )); + return Err(io::Error::new(io::ErrorKind::InvalidData, "legacy credential user id mismatch")); } write_user_credential(user_id, &parsed.to_canonical_string())?; fs::remove_file(legacy)?; diff --git a/iota-util/src/lib.rs b/iota-util/src/lib.rs index 1fdc888..5d32aef 100644 --- a/iota-util/src/lib.rs +++ b/iota-util/src/lib.rs @@ -1,6 +1,4 @@ pub mod crypto_helper; pub mod crypto_util; pub mod file_util; -pub mod mtp_compat; -pub mod route_target; pub mod tu; diff --git a/iota-util/src/mtp_compat.rs b/iota-util/src/mtp_compat.rs deleted file mode 100644 index 02aced4..0000000 --- a/iota-util/src/mtp_compat.rs +++ /dev/null @@ -1,67 +0,0 @@ -use mtp::codec::{CommunicationValue, DataValue}; -use mtp::type_map::DataTypeId; - -/* - * Keep legacy control-plane handlers source-compatible while they migrate to - * MTP's explicit optional routing fields. Relay handlers must use sender() and - * receiver() directly so an absent outer sender cannot become an identity. - */ -pub trait CommunicationValueCompat { - fn get_id(&self) -> u32; - fn get_sender(&self) -> u64; - fn get_receiver(&self) -> u64; -} - -impl CommunicationValueCompat for CommunicationValue { - fn get_id(&self) -> u32 { - self.id().unwrap_or_default() - } - - fn get_sender(&self) -> u64 { - self.sender().unwrap_or_default() - } - - fn get_receiver(&self) -> u64 { - self.receiver().unwrap_or_default() - } -} - -pub trait OptionalDataValueExt<'a> { - fn as_bool(self) -> Option; - fn as_str(self) -> Option<&'a str>; - fn as_string(self) -> Option; - fn as_number(self) -> Option; - fn as_signed_number(self) -> Option; - fn as_array(self) -> Option>; - fn as_container(self) -> Option>; -} - -impl<'a> OptionalDataValueExt<'a> for Option<&'a DataValue> { - fn as_bool(self) -> Option { - self.and_then(DataValue::as_bool) - } - - fn as_str(self) -> Option<&'a str> { - self.and_then(DataValue::as_str) - } - - fn as_string(self) -> Option { - self.and_then(DataValue::as_string) - } - - fn as_number(self) -> Option { - self.and_then(DataValue::as_number) - } - - fn as_signed_number(self) -> Option { - self.and_then(DataValue::as_signed_number) - } - - fn as_array(self) -> Option> { - self.and_then(DataValue::as_array) - } - - fn as_container(self) -> Option> { - self.and_then(DataValue::as_container) - } -} diff --git a/iota-util/src/route_target.rs b/iota-util/src/route_target.rs deleted file mode 100644 index 43e4249..0000000 --- a/iota-util/src/route_target.rs +++ /dev/null @@ -1,44 +0,0 @@ -const TARGET_KIND_MASK: u64 = 0xC000_0000_0000_0000; -const TARGET_ID_MASK: u64 = (1_u64 << 48) - 1; -const USER_TARGET_KIND: u64 = 0x4000_0000_0000_0000; -const IOTA_TARGET_KIND: u64 = 0x8000_0000_0000_0000; - -/* - * Relay receivers carry their namespace in the wire identity. This prevents - * a user ID and an Iota ID with the same numeric value from selecting the - * wrong connection at an Omikron. - */ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RouteTarget { - User(u64), - Iota(u64), -} - -impl RouteTarget { - pub fn wire_id(self) -> Option { - let (kind, id) = match self { - Self::User(id) => (USER_TARGET_KIND, id), - Self::Iota(id) => (IOTA_TARGET_KIND, id), - }; - (id > 0 && id <= TARGET_ID_MASK).then_some(kind | id) - } - - pub fn from_wire_id(value: u64) -> Option { - let id = value & TARGET_ID_MASK; - if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 { - return None; - } - - match value & TARGET_KIND_MASK { - USER_TARGET_KIND => Some(Self::User(id)), - IOTA_TARGET_KIND => Some(Self::Iota(id)), - _ => None, - } - } - - pub const fn id(self) -> u64 { - match self { - Self::User(id) | Self::Iota(id) => id, - } - } -} diff --git a/iota-util/src/tu.rs b/iota-util/src/tu.rs index 159bea6..e1a8334 100644 --- a/iota-util/src/tu.rs +++ b/iota-util/src/tu.rs @@ -47,10 +47,7 @@ impl fmt::Debug for TuCredential { impl TuCredential { pub fn parse(input: &str) -> Result { - let (identity, encoded_keyring) = input - .trim() - .split_once("::") - .ok_or(TuError::InvalidFormat)?; + let (identity, encoded_keyring) = input.trim().split_once("::").ok_or(TuError::InvalidFormat)?; if encoded_keyring.is_empty() || encoded_keyring.contains("::") { return Err(TuError::InvalidFormat); } @@ -63,11 +60,7 @@ impl TuCredential { return Err(TuError::InvalidUserId); } let keyring = keyring_from_base64(encoded_keyring).ok_or(TuError::InvalidKeyring)?; - Ok(Self { - user_id, - omega_host: omega_host.trim().to_owned(), - keyring, - }) + Ok(Self { user_id, omega_host: omega_host.trim().to_owned(), keyring }) } pub fn public_key_bundle(&self) -> PublicKeyBundle { @@ -75,12 +68,7 @@ impl TuCredential { } pub fn to_canonical_string(&self) -> String { - format!( - "{}@{}::{}", - self.user_id, - self.omega_host, - keyring_to_base64(&self.keyring) - ) + format!("{}@{}::{}", self.user_id, self.omega_host, keyring_to_base64(&self.keyring)) } } @@ -91,31 +79,16 @@ mod tests { #[test] fn round_trip_is_canonical() { - let credential = TuCredential { - user_id: 42, - omega_host: "omega.example:443".into(), - keyring: generate_keyring(), - }; + let credential = TuCredential { user_id: 42, omega_host: "omega.example:443".into(), keyring: generate_keyring() }; let parsed = TuCredential::parse(&credential.to_canonical_string()).unwrap(); assert_eq!(parsed.user_id, 42); assert_eq!(parsed.omega_host, "omega.example:443"); - assert_eq!( - parsed.to_canonical_string(), - credential.to_canonical_string() - ); + assert_eq!(parsed.to_canonical_string(), credential.to_canonical_string()); } #[test] fn rejects_malformed_credentials() { - for value in [ - "", - "1@omega", - "@omega::abc", - "0@omega::abc", - "281474976710656@omega::abc", - "1@::abc", - "1@omega::abc::def", - ] { + for value in ["", "1@omega", "@omega::abc", "0@omega::abc", "281474976710656@omega::abc", "1@::abc", "1@omega::abc::def"] { assert!(TuCredential::parse(value).is_err(), "{value}"); } } diff --git a/iota/src/cli_color.rs b/iota/src/cli_color.rs index c893805..2f7cb38 100644 --- a/iota/src/cli_color.rs +++ b/iota/src/cli_color.rs @@ -13,8 +13,10 @@ impl Default for ColorConfig { impl ColorConfig { pub fn new() -> Self { - let enabled = - env::var("NO_COLOR").is_err() && env::var("TERM").map(|t| t != "dumb").unwrap_or(true); + let enabled = env::var("NO_COLOR").is_err() + && env::var("TERM") + .map(|t| t != "dumb") + .unwrap_or(true); Self { enabled } } diff --git a/iota/src/main.rs b/iota/src/main.rs index d5c79d8..e05b63f 100644 --- a/iota/src/main.rs +++ b/iota/src/main.rs @@ -386,11 +386,17 @@ fn writable_socket_path(path: &Path) -> Result<(), StartupError> { fn print_help() { let color = cli_color::ColorConfig::new(); - println!("{}", cli_color::heading(&color, "Iota Operator Console")); + println!( + "{}", + cli_color::heading(&color, "Iota Operator Console") + ); println!(); println!("Usage: iota [OPTIONS] [COMMAND]"); println!(); - println!("{}", cli_color::info(&color, "Commands:")); + println!( + "{}", + cli_color::info(&color, "Commands:") + ); println!(" (no command) Launch the interactive dashboard"); println!(" status Show daemon status"); println!(" tasks List active tasks"); @@ -426,7 +432,10 @@ fn print_help() { println!(" completions Generate shell completions"); println!(" man Show the man page"); println!(); - println!("{}", cli_color::info(&color, "Options:")); + println!( + "{}", + cli_color::info(&color, "Options:") + ); println!(" --theme Theme: monospace, binary, ansi, surface"); println!(" --output Output format: text, json, yaml, table"); println!(" --color Color: auto, always, never"); @@ -436,7 +445,10 @@ fn print_help() { println!(" -h, --help Show help"); println!(" -V, --version Show version"); println!(); - println!("{}", cli_color::info(&color, "Examples:")); + println!( + "{}", + cli_color::info(&color, "Examples:") + ); println!(" iota Launch the interactive dashboard"); println!(" iota status Show daemon status"); println!(" iota users list --output=json List users in JSON format"); @@ -446,13 +458,19 @@ fn print_help() { println!(" iota logs --limit 50 Show last 50 log entries"); println!(" iota completions bash Generate bash completions"); println!(); - println!("{}", cli_color::info(&color, "Exit Codes:")); + println!( + "{}", + cli_color::info(&color, "Exit Codes:") + ); println!(" 0 Success"); println!(" 1 General error"); println!(" 2 Invalid command or arguments"); println!(" 130 Interrupted (Ctrl+C)"); println!(); - println!("{}", cli_color::muted(&color, "Environment Variables:")); + println!( + "{}", + cli_color::muted(&color, "Environment Variables:") + ); println!(" NO_COLOR Disable colored output when set"); println!(" TERM Terminal type (dumb disables colors)"); println!(" IOTA_THEME Default theme override"); @@ -548,26 +566,13 @@ async fn run_command( Command::Tasks => LocalRequest::ListTasks, Command::UsersList => LocalRequest::ListUsers, Command::UsersShow { user_id } => LocalRequest::GetUser { user_id }, - Command::UsersAdd { - username: Some(username), - tu: None, - } => LocalRequest::CreateUser { username }, - Command::UsersAdd { - username: None, - tu: Some(path), - } => { - let contents = std::fs::read_to_string(&path).map_err(|error| { - StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())) - })?; - iota_util::tu::TuCredential::parse(&contents).map_err(|error| { - StartupError::InvalidCommand(format!( - "Invalid credential {}: {error}", - path.display() - )) - })?; - LocalRequest::AttachUserFromTu { - credential: iota_ipc::SecretString(contents), - } + Command::UsersAdd { username: Some(username), tu: None } => LocalRequest::CreateUser { username }, + Command::UsersAdd { username: None, tu: Some(path) } => { + let contents = std::fs::read_to_string(&path) + .map_err(|error| StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())))?; + iota_util::tu::TuCredential::parse(&contents) + .map_err(|error| StartupError::InvalidCommand(format!("Invalid credential {}: {error}", path.display())))?; + LocalRequest::AttachUserFromTu { credential: iota_ipc::SecretString(contents) } } Command::UsersAdd { .. } => { return Err(StartupError::InvalidCommand( @@ -578,43 +583,22 @@ async fn run_command( user_id, confirmed: true, } => LocalRequest::ReleaseUser { user_id }, - Command::UsersPurgeData { - user_id, - confirmed: true, - } => LocalRequest::PurgeUserData { user_id }, - Command::UsersCompleteDelete { - user_id, - tu, - confirmed: true, - } => { + Command::UsersPurgeData { user_id, confirmed: true } => LocalRequest::PurgeUserData { user_id }, + Command::UsersCompleteDelete { user_id, tu, confirmed: true } => { let credential = match tu { Some(path) => { - let contents = std::fs::read_to_string(&path).map_err(|error| { - StartupError::InvalidCommand(format!( - "Cannot read {}: {error}", - path.display() - )) - })?; - let parsed = - iota_util::tu::TuCredential::parse(&contents).map_err(|error| { - StartupError::InvalidCommand(format!( - "Invalid credential {}: {error}", - path.display() - )) - })?; + let contents = std::fs::read_to_string(&path) + .map_err(|error| StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())))?; + let parsed = iota_util::tu::TuCredential::parse(&contents) + .map_err(|error| StartupError::InvalidCommand(format!("Invalid credential {}: {error}", path.display())))?; if parsed.user_id != user_id { - return Err(StartupError::InvalidCommand( - "credential user ID does not match complete-delete target".into(), - )); + return Err(StartupError::InvalidCommand("credential user ID does not match complete-delete target".into())); } Some(iota_ipc::SecretString(contents)) } None => None, }; - LocalRequest::CompleteDeleteUser { - user_id, - credential, - } + LocalRequest::CompleteDeleteUser { user_id, credential } } Command::OmikronReconnect => LocalRequest::ReconnectOmikron, Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity, @@ -637,12 +621,8 @@ async fn run_command( Command::UsersRelease { confirmed: false, .. } - | Command::UsersPurgeData { - confirmed: false, .. - } - | Command::UsersCompleteDelete { - confirmed: false, .. - } + | Command::UsersPurgeData { confirmed: false, .. } + | Command::UsersCompleteDelete { confirmed: false, .. } | Command::IdentityRotate { confirmed: false } | Command::RegenerateKeys { confirmed: false } | Command::DaemonRestart { confirmed: false } @@ -735,15 +715,15 @@ async fn run_command( ); } ResponsePayload::UserRemoved { user_id } => { - println!("{} {}", cli_color::warning(&color, "Removed user"), user_id); - } - ResponsePayload::UserDataPurged { user_id } => { println!( - "{} hosted data for {}. Account remains managed by this Iota.", - cli_color::success(&color, "Purged"), + "{} {}", + cli_color::warning(&color, "Removed user"), user_id ); } + ResponsePayload::UserDataPurged { user_id } => { + println!("{} hosted data for {}. Account remains managed by this Iota.", cli_color::success(&color, "Purged"), user_id); + } ResponsePayload::Acknowledged { message } => { println!("{}", message); } @@ -760,7 +740,11 @@ async fn run_command( status.connected ); if let Some(id) = status.iota_id { - println!("{}: {}", cli_color::info(&color, "Iota ID"), id); + println!( + "{}: {}", + cli_color::info(&color, "Iota ID"), + id + ); } } ResponsePayload::Components(components) => { @@ -772,7 +756,9 @@ async fn run_command( } else { for comp in &components { let (status_str, style) = match comp.status { - iota_ipc::HealthStatus::Healthy => ("healthy", cli_color::SUCCESS), + iota_ipc::HealthStatus::Healthy => { + ("healthy", cli_color::SUCCESS) + } iota_ipc::HealthStatus::Degraded => { ("degraded", cli_color::WARNING) } @@ -825,9 +811,15 @@ async fn run_command( } ResponsePayload::UpdateStatus(status) => { if status.available { - println!("{}", cli_color::success(&color, "Update available.")); + println!( + "{}", + cli_color::success(&color, "Update available.") + ); } else { - println!("{}", cli_color::info(&color, "Up to date.")); + println!( + "{}", + cli_color::info(&color, "Up to date.") + ); } } ResponsePayload::Communities(communities) => { @@ -835,7 +827,11 @@ async fn run_command( println!("{}", cli_color::muted(&color, "No communities.")); } else { for c in &communities { - println!("{} ({})", cli_color::heading(&color, &c.title), c.name); + println!( + "{} ({})", + cli_color::heading(&color, &c.title), + c.name + ); } } } @@ -909,12 +905,7 @@ fn render_table(payload: &ResponsePayload) { iota_ipc::HealthStatus::Failed => "failed", }; let message = comp.message.as_deref().unwrap_or("-"); - println!( - "{:<20} {:<10} {}", - format!("{:?}", comp.id), - status_str, - message - ); + println!("{:<20} {:<10} {}", format!("{:?}", comp.id), status_str, message); } } ResponsePayload::Communities(communities) => { @@ -933,14 +924,8 @@ fn render_table(payload: &ResponsePayload) { println!("No log entries."); return; } - println!( - "{:<20} {:<6} {:<12} {}", - "TIMESTAMP", "LEVEL", "SENDER", "MESSAGE" - ); - println!( - "{:<20} {:<6} {:<12} {}", - "--------", "--------", "--------", "--------" - ); + println!("{:<20} {:<6} {:<12} {}", "TIMESTAMP", "LEVEL", "SENDER", "MESSAGE"); + println!("{:<20} {:<6} {:<12} {}", "--------", "--------", "--------", "--------"); for entry in &logs.entries { let level = if entry.is_error { "ERR" } else { "INF" }; println!( diff --git a/iota/src/startup_error.rs b/iota/src/startup_error.rs index 4c46c32..583c281 100644 --- a/iota/src/startup_error.rs +++ b/iota/src/startup_error.rs @@ -43,24 +43,34 @@ impl StartupError { Self::SystemPermissionDenied(_) => { Some("Run with appropriate privileges or use a user-level daemon instead.") } - Self::SocketPermissionDenied(_) => Some( - "Check file permissions on the socket or ensure the daemon is running as your user.", - ), - Self::IpcTimedOut(_) => Some( - "The daemon may be starting up. Wait a moment and try again, or check daemon logs.", - ), - Self::ProtocolMismatch { .. } => Some("Update your CLI or daemon to match versions."), - Self::DaemonExited { .. } => Some("Restart the daemon with `iota daemon restart`."), - Self::IpcBindUnavailable(_) => Some( - "Another instance may be running. Stop it first or use a different socket path.", - ), + Self::SocketPermissionDenied(_) => { + Some( + "Check file permissions on the socket or ensure the daemon is running as your user.", + ) + } + Self::IpcTimedOut(_) => { + Some( + "The daemon may be starting up. Wait a moment and try again, or check daemon logs.", + ) + } + Self::ProtocolMismatch { .. } => { + Some("Update your CLI or daemon to match versions.") + } + Self::DaemonExited { .. } => { + Some("Restart the daemon with `iota daemon restart`.") + } + Self::IpcBindUnavailable(_) => { + Some("Another instance may be running. Stop it first or use a different socket path.") + } Self::Terminal(_) => { Some("Use a terminal that supports interactive mode, or run commands headlessly.") } - Self::Consent(_) => Some( - "Run `iota terms accept` in an interactive terminal to review and accept terms.", - ), - Self::InvalidCommand(_) => Some("Run `iota --help` to see available commands."), + Self::Consent(_) => { + Some("Run `iota terms accept` in an interactive terminal to review and accept terms.") + } + Self::InvalidCommand(_) => { + Some("Run `iota --help` to see available commands.") + } _ => None, } } @@ -139,16 +149,8 @@ mod tests { } #[test] fn most_errors_have_suggestions() { - assert!( - StartupError::DaemonExecutableMissing(PathBuf::from("iota-daemon")) - .suggestion() - .is_some() - ); - assert!( - StartupError::IpcTimedOut(PathBuf::from("/tmp/iota.sock")) - .suggestion() - .is_some() - ); + assert!(StartupError::DaemonExecutableMissing(PathBuf::from("iota-daemon")).suggestion().is_some()); + assert!(StartupError::IpcTimedOut(PathBuf::from("/tmp/iota.sock")).suggestion().is_some()); assert!(StartupError::Cancelled.suggestion().is_none()); } } diff --git a/omikron-connector/Cargo.toml b/omikron-connector/Cargo.toml index 31796b0..1e3e21d 100644 --- a/omikron-connector/Cargo.toml +++ b/omikron-connector/Cargo.toml @@ -14,7 +14,6 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ "client", "crypto", "files", - "raw", ] } dashmap = "6.2.1" diff --git a/omikron-connector/src/omikron_connection.rs b/omikron-connector/src/omikron_connection.rs index 1e1f5c7..a555398 100755 --- a/omikron-connector/src/omikron_connection.rs +++ b/omikron-connector/src/omikron_connection.rs @@ -1,9 +1,9 @@ use dashmap::{DashMap, DashSet}; use iota_logger::{log, log_cv_in, log_cv_out, log_t}; use iota_state::AppState; -use iota_storage::util::chat_files; +use iota_storage::util::chat_files::{self, MessageState, change_message_state}; use iota_storage::util::config_util::{CONFIG, modify_config}; -use iota_storage::util::{relay_queue, relay_replay}; +use iota_storage::util::e2ee_storage::{self, PendingChatSecretForward, StoredChatSecret}; use iota_util::crypto_helper::{self, keyring_from_base64}; use iota_util::crypto_util::{self}; use mtp::client::{Client, ClientConfig, MTPConnection, Policy, SendMode, Sender}; @@ -11,10 +11,7 @@ use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::crypto::{Keyring, PublicKeyBundle}; use std::env; use std::path::{Path, PathBuf}; -use std::sync::{ - Arc, LazyLock, - atomic::{AtomicU32, Ordering}, -}; +use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch}; use tokio::task::JoinHandle; @@ -27,11 +24,68 @@ use crate::omega_discovery; use iota_connection::message_common::*; use iota_connection::message_handlers; -use iota_connection::relay::{ - RelayValidationError, forward_verified_relay, open_verified_relay_content, - verify_relay_metadata, -}; -use iota_util::route_target::RouteTarget; + +fn pending_chat_secret_forward_from_cv( + cv: &CommunicationValue, +) -> Option { + let recipient = chat_secret_recipients(cv)?.into_iter().next()?; + Some(PendingChatSecretForward { + recipient_user_id: recipient.user_id, + chat_id: data_string(cv, DataType::ChatId)?, + sender_user_id: cv.get_sender().to_string(), + secret_id: data_string(cv, DataType::SecretId)?, + version: data_i64(cv, DataType::VersionNumber)?, + encrypted_secret: recipient.encrypted_secret, + kem_ciphertext: recipient.kem_ciphertext, + wrapping_scheme: data_string(cv, DataType::WrappingScheme)?, + created_at: data_i64(cv, DataType::CreatedAt).unwrap_or_else(now_millis_i64), + }) +} + +fn chat_secret_forward_cv(record: &PendingChatSecretForward) -> CommunicationValue { + let recipient = typed_container(vec![ + ( + DataType::UserId, + DataValue::Str(record.recipient_user_id.clone()), + ), + ( + DataType::EncryptedSecret, + DataValue::Bytes(record.encrypted_secret.clone()), + ), + ( + DataType::KemCiphertext, + DataValue::Bytes(record.kem_ciphertext.clone()), + ), + ]); + + CommunicationValue::new(CommunicationType::SetChatSecret) + .with_sender(record.sender_user_id.parse::().unwrap_or(0)) + .with_receiver(record.recipient_user_id.parse::().unwrap_or(0)) + .add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id.clone())) + .add_typed_default( + DataType::SenderUserId, + DataValue::Str(record.sender_user_id.clone()), + ) + .add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id.clone())) + .add_typed_default( + DataType::VersionNumber, + DataValue::SignedNumber(record.version as i128), + ) + .add_typed_default( + DataType::WrappingScheme, + DataValue::Str(record.wrapping_scheme.clone()), + ) + .add_typed_default( + DataType::CreatedAt, + DataValue::SignedNumber(record.created_at as i128), + ) + .add_typed_default(DataType::Recipients, DataValue::Array(vec![recipient])) +} + +// Helper function to check if read receipts are enabled globally +async fn is_read_receipts_enabled() -> bool { + CONFIG.load().read_receipts_enabled +} // ============================================================================ // Configuration @@ -59,8 +113,6 @@ const MAINTENANCE_INTERVAL: Duration = Duration::from_secs(5); const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); const TASK_MAX_AGE: Duration = Duration::from_secs(60); const MAX_CONCURRENT_HANDLERS: usize = 20; -const RELAY_RETENTION_MILLIS: i64 = 30 * 24 * 60 * 60 * 1000; -static NEXT_RELAY_FRAME_ID: AtomicU32 = AtomicU32::new(1); // ============================================================================ // Waiting Task System @@ -119,7 +171,6 @@ pub struct OmikronConnection { shutdown_tx: Arc>>>, reconnect_on_close: Arc>, auth_failure: Arc>>, - keyring: Arc>>>, pub app_challenges: Arc>, pub app_sessions: Arc>, handler_semaphore: Arc, @@ -128,14 +179,6 @@ pub struct OmikronConnection { pub(crate) app: Arc>, } -fn ensure_relay_frame_id(frame: CommunicationValue) -> CommunicationValue { - if frame.id().is_some_and(|id| id != 0) { - return frame; - } - let id = NEXT_RELAY_FRAME_ID.fetch_add(1, Ordering::Relaxed).max(1); - frame.with_id(id) -} - impl OmikronConnection { pub fn new(active_tasks: Arc>, app: Arc>) -> Self { Self::with_cancellation(CancellationToken::new(), active_tasks, app) @@ -160,7 +203,6 @@ impl OmikronConnection { shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), reconnect_on_close: Arc::new(RwLock::new(true)), auth_failure: Arc::new(RwLock::new(None)), - keyring: Arc::new(RwLock::new(None)), app_challenges: Arc::new(DashMap::new()), app_sessions: Arc::new(DashMap::new()), handler_semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)), @@ -286,8 +328,7 @@ impl OmikronConnection { self.set_state(ConnectionState::Connecting).await; log_t!("omikron_connecting"); - let keyring = Arc::new(self.load_or_migrate_keyring().await); - *self.keyring.write().await = Some(keyring.clone()); + let keyring = self.load_or_migrate_keyring().await; let existing_iota_id = CONFIG.load().iota_id; @@ -554,15 +595,6 @@ impl OmikronConnection { let result = connection.receive().await; match result { Ok(cv) => { - if cv.is_type(CommunicationType::Relay) { - let permit = self.handler_semaphore.clone().acquire_owned().await; - let self_clone = self.clone(); - tokio::spawn(async move { - let _permit = permit; - self_clone.handle_relay(cv).await; - }); - continue; - } let msg_id = cv.get_id(); if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { if (task.task)(cv.clone()) { @@ -618,452 +650,188 @@ impl OmikronConnection { self.app.lock().unwrap().push_ping_val(ping_ms as f64); } - self.flush_pending_relays().await; - if let Err(error) = relay_replay::prune_completed( - now_millis_i64().saturating_sub(RELAY_RETENTION_MILLIS), - ) { - log!("Relay replay cleanup failed: {}", error); + self.flush_pending_chat_secret_forwards().await; + } + } + + async fn forward_chat_secret(&self, cv: &CommunicationValue) -> bool { + self.await_response(cv, Some(Duration::from_secs(10))) + .await + .is_ok() + } + + async fn store_pending_chat_secret_forward(&self, cv: &CommunicationValue) { + let Some(record) = pending_chat_secret_forward_from_cv(cv) else { + return; + }; + let _ = e2ee_storage::put_pending_chat_secret_forward(record); + } + + async fn flush_pending_chat_secret_forwards(&self) { + let Ok(records) = e2ee_storage::get_pending_chat_secret_forwards(100) else { + return; + }; + + for record in records { + let Ok(recipient) = record.recipient_user_id.parse::() else { + let _ = e2ee_storage::delete_pending_chat_secret_forward( + &record.recipient_user_id, + &record.chat_id, + &record.secret_id, + ); + continue; + }; + + let message = chat_secret_forward_cv(&record).with_receiver(recipient); + if self.forward_chat_secret(&message).await { + let _ = e2ee_storage::delete_pending_chat_secret_forward( + &record.recipient_user_id, + &record.chat_id, + &record.secret_id, + ); } } } - async fn resolve_relay_signing_keys( + async fn forward_message_live( &self, - signer_id: u64, - ) -> Result, RelayValidationError> { - if let Some(user) = iota_storage::users::user_manager::get_user(signer_id as i64) { - let key = iota_util::crypto_helper::public_key_bundle_from_base64(&user.public_key) - .ok_or_else(|| { - RelayValidationError::KeyLookup("stored user key is invalid".into()) - })?; - return Ok(vec![key]); - } - - let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( - DataType::UserId, - DataValue::UnsignedNumber(signer_id as u128), - ); - let response = self - .await_response(&request, Some(Duration::from_secs(10))) - .await - .map_err(RelayValidationError::KeyLookup)?; - if !response.is_type(CommunicationType::GetUserData) { - return Err(RelayValidationError::KeyLookup( - "Omega returned an unexpected user lookup response".into(), + message_id: u32, + receiver_id: u64, + sender_id: i64, + timestamp: i64, + content: &str, + height: i64, + reply_to: Option, + ) -> Option { + let mut msg_fields = vec![ + (DataType::Content, DataValue::Str(content.to_string())), + ( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ), + (DataType::Height, DataValue::SignedNumber(height as i128)), + ]; + if let Some(rt) = reply_to { + msg_fields.push(( + DataType::ReplyId, + DataValue::UnsignedNumber(rt as u64 as u128), )); } - let public_key = response - .get_data(DataType::PublicKey) - .and_then(|value| value.as_str()) - .ok_or_else(|| RelayValidationError::KeyLookup("Omega returned no user key".into()))?; - let key = iota_util::crypto_helper::public_key_bundle_from_base64(public_key).ok_or_else( - || RelayValidationError::KeyLookup("Omega returned an invalid user key".into()), - )?; - Ok(vec![key]) - } - pub async fn hosting_iota_for_user(&self, user_id: u64) -> Result { - if iota_storage::users::user_manager::get_user(user_id as i64).is_some() { - return CONFIG - .load() - .iota_id - .ok_or_else(|| "Iota identity is not configured".into()); - } + let user_forward = CommunicationValue::new(CommunicationType::MessageLive) + .with_id(message_id) + .with_receiver(receiver_id) + .add_typed_default( + DataType::SenderId, + DataValue::SignedNumber(sender_id as i128), + ) + .add_typed_default(DataType::Message, typed_container(msg_fields)); - let request = CommunicationValue::new(CommunicationType::GetUserData) - .add_typed_default(DataType::UserId, DataValue::UnsignedNumber(user_id as u128)); - let response = self - .await_response(&request, Some(Duration::from_secs(10))) - .await?; - response - .get_data(DataType::IotaId) - .and_then(|value| value.as_number()) - .and_then(|value| u64::try_from(value).ok()) - .filter(|value| *value > 0) - .ok_or_else(|| "Omega returned no hosting Iota for the user".into()) - } - - async fn send_relay_response(&self, frame_id: Option, response_type: CommunicationType) { - if let Some(frame_id) = frame_id { - let response = CommunicationValue::new(response_type).with_id(frame_id); - if let Err(error) = self.send_message(&response).await { - log!("Relay response could not be sent: {}", error); + match self + .await_response(&user_forward, Some(Duration::from_secs(3))) + .await + { + Ok(user_resp) => { + let ms_raw = user_resp + .get_data(DataType::MessageState) + .as_string() + .unwrap_or_else(|| "".to_string()); + Some(MessageState::from_str(&ms_raw).upgrade(MessageState::Received)) } + Err(_) => None, } } - async fn handle_relay(self: Arc, frame: CommunicationValue) { - let frame = ensure_relay_frame_id(frame); - let incoming_frame_id = frame.id(); - let Some(local_iota_id) = CONFIG.load().iota_id else { - log!("Rejecting Relay because this Iota has no registered identity"); - self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInvalidData) - .await; - return; - }; - let Some(keyring) = self.keyring.read().await.as_ref().cloned() else { - log!("Rejecting Relay because the Iota keyring is unavailable"); - self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInternal) - .await; - return; - }; - - let resolver_connection = self.clone(); - let verified = verify_relay_metadata( - &frame, - local_iota_id, - &keyring, - move |signer_id| async move { - resolver_connection - .resolve_relay_signing_keys(signer_id) - .await - }, - ) - .await; - - let verified = match verified { - Ok(value) => value, - Err(error) => { - log!("Relay metadata verification failed: {}", error); - self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInvalidData) - .await; - return; - } - }; - let signer_is_local = - iota_storage::users::user_manager::get_user(verified.context.signer_id as i64) - .is_some(); - let recipient_is_local = - iota_storage::users::user_manager::get_user(verified.context.final_recipient_id as i64) - .is_some(); - if !signer_is_local && !recipient_is_local { - log!( - "Rejecting Relay with no local origin or destination: signer {}, recipient {}", - verified.context.signer_id, - verified.context.final_recipient_id, + async fn forward_to_remote_iota( + &self, + cv: &CommunicationValue, + sender_id: i64, + receiver_id: i64, + timestamp: i64, + content: &str, + height: i64, + reply_to: Option, + ) { + let mut fw_msg = CommunicationValue::new(CommunicationType::MessageOtherIota) + .with_id(cv.get_id()) + .with_receiver(receiver_id as u64) + .with_sender(sender_id as u64) + .add_typed_default(DataType::Height, DataValue::SignedNumber(height as i128)) + .add_typed_default(DataType::Content, DataValue::Str(content.to_string())) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ); + if let Some(rt) = reply_to { + fw_msg = fw_msg.add_typed_default( + DataType::ReplyId, + DataValue::UnsignedNumber(rt as u64 as u128), ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) - .await; - return; } - let frame_bytes = match frame.clone().without_id().to_bytes() { - Ok(bytes) => bytes, - Err(error) => { - log!( - "Relay could not be serialized for durable acceptance: {}", - error - ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - }; - let type_map_version = verified.context.type_map.version.to_string(); - let frame_id = frame.id().unwrap_or_default(); - let reservation = match relay_replay::reserve( - verified.context.signer_id, - &verified.context.message_id, - verified.context.created_at, - verified.context.final_recipient_id, - &frame_bytes, - frame_id, - &type_map_version, - ) { - Ok(value) => value, - Err(error) => { - log!("Relay durable acceptance failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - }; - let already_applied = match reservation { - relay_replay::RelayReservation::New => false, - relay_replay::RelayReservation::Existing { - frame_matches: false, - .. - } => { - log!( - "Rejecting Relay identity collision for signer {} and message {}", - verified.context.signer_id, - verified.context.message_id - ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) - .await; - return; - } - relay_replay::RelayReservation::Existing { ref state, .. } if state == "delivered" => { - self.send_relay_response(frame.id(), CommunicationType::Success) - .await; - return; - } - relay_replay::RelayReservation::Existing { ref state, .. } - if state == "applied" || state == "queued" => - { - true - } - relay_replay::RelayReservation::Existing { ref state, .. } if state == "rejected" => { - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) - .await; - return; - } - relay_replay::RelayReservation::Existing { .. } => false, - }; + match self + .await_response(&fw_msg, Some(Duration::from_secs(10))) + .await + { + Ok(resp) => { + let ms_raw = resp + .get_data(DataType::MessageState) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); - if signer_is_local && !recipient_is_local { - let router = match self - .hosting_iota_for_user(verified.context.final_recipient_id) - .await - { - Ok(destination_iota) => destination_iota, - Err(error) => { - log!("Relay origin route lookup failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorNoIota) - .await; - return; - } - }; - let forwarded = match forward_verified_relay(&frame, RouteTarget::Iota(router)) { - Ok(value) => value, - Err(error) => { - log!("Relay origin forwarding validation failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) - .await; - return; - } - }; - let bytes = match forwarded.to_bytes() { - Ok(bytes) => bytes, - Err(error) => { - log!("Relay origin retry could not be serialized: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - }; - if let Err(error) = relay_queue::enqueue( - RouteTarget::Iota(router), - &bytes, - now_millis_i64(), - frame_id, - &type_map_version, - ) { - log!("Relay origin retry queue failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - if let Err(error) = relay_replay::mark_state( - verified.context.signer_id, - &verified.context.message_id, - "queued", - ) { - log!("Relay origin state update failed: {}", error); - } - match self - .await_response(&forwarded, Some(Duration::from_secs(20))) - .await - { - Ok(response) if response.is_type(CommunicationType::Success) => { - if let Err(error) = relay_queue::acknowledge_iota(router, frame_id) { - log!( - "Relay origin acknowledgement could not clear the queue: {}", - error - ); - } - if let Err(error) = relay_replay::mark_state( - verified.context.signer_id, - &verified.context.message_id, - "delivered", - ) { - log!("Relay origin delivery state update failed: {}", error); - } - self.send_relay_response(frame.id(), CommunicationType::Success) - .await; - } - Ok(response) => { - log!("Relay origin route returned {}", response.get_type()); - self.send_relay_response( - frame.id(), - response - .get_comm_type_enum() - .unwrap_or(CommunicationType::ErrorInternal), + let _ = + chat_files::change_message_state(timestamp, sender_id, receiver_id, ms.clone()); + + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), ) .await; - } - Err(error) => { - log!("Relay origin forwarding failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - } } - return; - } - - let destination = verified.context.final_recipient_id; - let forwarded = match forward_verified_relay(&frame, RouteTarget::User(destination)) { - Ok(value) => value, - Err(error) => { - log!("Relay forwarding validation failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) - .await; - return; - } - }; - let bytes = match forwarded.to_bytes() { - Ok(bytes) => bytes, - Err(error) => { - log!( - "Relay could not be serialized for client delivery: {}", - error + Err(_) => { + let _ = chat_files::change_message_state( + timestamp, + sender_id, + receiver_id, + MessageState::Sent, ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - }; - if let Err(error) = relay_queue::enqueue( - RouteTarget::User(destination), - &bytes, - now_millis_i64(), - forwarded.id().unwrap_or_default(), - &type_map_version, - ) { - log!("Relay could not be queued for client delivery: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - if !already_applied { - let content = match open_verified_relay_content( - &verified, - &[&keyring], - verified.context.final_recipient_id, - ) { - Ok(value) => value, - Err(error) => { - log!("Relay content verification failed: {}", error); - if let Err(queue_error) = - relay_queue::remove_for_frame(RouteTarget::User(destination), frame_id) - { - log!( - "Relay invalid-content queue cleanup failed: {}", - queue_error - ); - } - let _ = relay_replay::mark_state( - verified.context.signer_id, - &verified.context.message_id, - "rejected", - ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) - .await; - return; - } - }; - if let Err(error) = - message_handlers::apply_verified_relay_content(&verified.context, &content) - { - log!("Relay application dispatch failed: {}", error); - if let Err(queue_error) = - relay_queue::remove_for_frame(RouteTarget::User(destination), frame_id) - { - log!("Relay application queue cleanup failed: {}", queue_error); - } - let _ = relay_replay::mark_state( - verified.context.signer_id, - &verified.context.message_id, - "rejected", - ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) .await; - return; - } - if let Err(error) = relay_replay::mark_state( - verified.context.signer_id, - &verified.context.message_id, - "applied", - ) { - log!("Relay application state update failed: {}", error); - self.send_relay_response(frame.id(), CommunicationType::ErrorInternal) - .await; - return; - } - } - - if let Err(error) = relay_replay::mark_state( - verified.context.signer_id, - &verified.context.message_id, - "queued", - ) { - log!("Relay queue state update failed: {}", error); - } - self.send_relay_response(frame.id(), CommunicationType::Success) - .await; - if let Err(error) = self.send_message(&forwarded).await { - log!("Relay delivery to local client failed: {}", error); - } - } - - async fn flush_pending_relays(&self) { - let Ok(records) = relay_queue::list(100) else { - return; - }; - for record in records { - let Some(version) = mtp::type_map::Version::parse(&record.type_map_version) else { - log!( - "Retaining pending Relay {} with invalid type-map version {}", - record.id, - record.type_map_version - ); - continue; - }; - let type_map = mtp::codec::TypeMap::new(version); - let Ok(frame) = CommunicationValue::from_bytes_with(&record.frame, &type_map) else { - log!("Retaining pending Relay {} with invalid frame", record.id); - continue; - }; - let Ok(forwarded) = forward_verified_relay(&frame, record.target) else { - log!( - "Retaining pending Relay {} with invalid route target", - record.id - ); - continue; - }; - match record.target { - RouteTarget::Iota(destination_iota) => { - match self - .await_response(&forwarded, Some(Duration::from_secs(20))) - .await - { - Ok(response) if response.is_type(CommunicationType::Success) => { - if let Err(error) = - relay_queue::acknowledge_iota(destination_iota, record.frame_id) - { - log!( - "Pending Relay {} acknowledgement could not clear the queue: {}", - record.id, - error - ); - } - } - Ok(response) => log!( - "Pending Relay {} route returned {}", - record.id, - response.get_type() - ), - Err(error) => { - log!("Pending Relay {} delivery failed: {}", record.id, error) - } - } - } - RouteTarget::User(_) => { - if let Err(error) = self.send_message(&forwarded).await { - log!("Pending Relay {} delivery failed: {}", record.id, error); - } - } } } } @@ -1075,27 +843,6 @@ impl OmikronConnection { pub async fn handle_message(self: Arc, cv: CommunicationValue) { log_cv_in!(&cv); - if cv.is_type(CommunicationType::Success) - && let Some(frame_id) = cv.id() - && let Some(destination_id) = cv - .get_data(DataType::UserId) - .as_number() - .and_then(|value| u64::try_from(value).ok()) - { - match relay_queue::acknowledge(destination_id, frame_id) { - Ok(true) => { - if let Err(error) = - relay_replay::mark_delivered_for_frame(destination_id, frame_id) - { - log!("Relay delivery state update failed: {}", error); - } - return; - } - Ok(false) => {} - Err(error) => log!("Relay delivery acknowledgement failed: {}", error), - } - } - let msg_id = cv.get_id(); if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { @@ -1108,22 +855,6 @@ impl OmikronConnection { } async fn handle_message_impl(self: Arc, cv: CommunicationValue) { - if cv.is_type(CommunicationType::Relay) { - self.handle_relay(cv).await; - return; - } - - if matches!( - iota_connection::relay::message_security_class(&cv), - iota_connection::relay::MessageSecurityClass::RelayOnly - ) { - log!("Rejecting sender-based application mutation outside Relay"); - let _ = self - .send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - } - macro_rules! dispatch { ($ty:ident, $method:ident) => { if cv.is_type(CommunicationType::$ty) { @@ -1133,7 +864,9 @@ impl OmikronConnection { }; } + dispatch!(SetChatSecret, handle_set_chat_secret); dispatch!(GetChatSecret, handle_get_chat_secret); + dispatch!(ChatSecretForward, handle_chat_secret_forward); dispatch!(AppIdentification, handle_app_identification); dispatch!(AppChallengeResponse, handle_app_challenge_response); dispatch!(SaveAppData, handle_save_app_data); @@ -1143,12 +876,14 @@ impl OmikronConnection { dispatch!(ClientConnected, handle_client_connected); dispatch!(ClientStateAck, handle_client_state_ack); dispatch!(MessageState, handle_message_state); + dispatch!(MessageSend, handle_message_send); dispatch!(MessageEdit, handle_message_edit); dispatch!(MessageEditLive, handle_message_edit_live); dispatch!(MessageReactionAdd, handle_message_reaction_add); dispatch!(MessageReactionRemove, handle_message_reaction_remove); dispatch!(MessageReactionLive, handle_message_reaction_live); dispatch!(MessageDeleteLive, handle_message_delete_live); + dispatch!(MessageOtherIota, handle_message_other_iota); dispatch!(MessageGet, handle_message_get); dispatch!(MessagesGet, handle_messages_get); dispatch!(GetChats, handle_get_chats); @@ -1196,12 +931,123 @@ impl OmikronConnection { let _ = self.send_message(&acknowledgement).await; } + async fn handle_set_chat_secret(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender().to_string(); + let recipients = match chat_secret_recipients(cv) { + Some(recipients) => recipients, + None => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; + let now = now_millis_i64(); + let chat_id = data_string(cv, DataType::ChatId); + let secret_id = data_string(cv, DataType::SecretId); + let version = data_i64(cv, DataType::VersionNumber); + let wrapping_scheme = data_string(cv, DataType::WrappingScheme); + let created_at = data_i64(cv, DataType::CreatedAt).unwrap_or(now); + + let Some((((chat_id, secret_id), version), wrapping_scheme)) = + chat_id.zip(secret_id).zip(version).zip(wrapping_scheme) + else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + + let mut non_local_forwards: Vec = Vec::new(); + + for recipient in &recipients { + let recipient_id = recipient.user_id.parse::().unwrap_or(0); + let is_local = iota_storage::users::user_manager::get_user(recipient_id).is_some(); + + if is_local { + if e2ee_storage::put_chat_secret(StoredChatSecret { + user_id: recipient.user_id.clone(), + chat_id: chat_id.clone(), + secret_id: secret_id.clone(), + version, + encrypted_secret: recipient.encrypted_secret.clone(), + kem_ciphertext: recipient.kem_ciphertext.clone(), + wrapping_scheme: wrapping_scheme.clone(), + created_at, + updated_at: now, + }) + .is_err() + { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + continue; + } + + if recipient.user_id != sender_id { + non_local_forwards.push(set_chat_secret_cv_for_recipient(cv, recipient)); + } + } + + if !non_local_forwards.is_empty() { + let mut handles = Vec::new(); + for forward in &non_local_forwards { + let self_clone = self.clone(); + let fwd = forward.clone(); + handles.push(tokio::spawn(async move { + self_clone.forward_chat_secret(&fwd).await + })); + } + + for (forward, handle) in non_local_forwards.into_iter().zip(handles) { + match handle.await { + Ok(true) => {} + _ => self.store_pending_chat_secret_forward(&forward).await, + } + } + } + + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; + } + async fn handle_get_chat_secret(self: Arc, cv: &CommunicationValue) { let _ = self .send_message(&message_handlers::handle_get_chat_secret(cv)) .await; } + async fn handle_chat_secret_forward(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender().to_string(); + let recipient_user_id = data_string(cv, DataType::RecipientUserId).unwrap_or_default(); + if data_string(cv, DataType::SenderUserId).as_deref() != Some(sender_id.as_str()) + || recipient_user_id.is_empty() + || pending_chat_secret_forward_from_cv(cv).is_none() + { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + + let forward = cv + .clone() + .with_receiver(recipient_user_id.parse::().unwrap_or(0)); + if self.forward_chat_secret(&forward).await { + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; + } else { + self.store_pending_chat_secret_forward(cv).await; + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; + } + } + async fn handle_app_identification(self: Arc, cv: &CommunicationValue) { let sender_id = cv.get_sender(); let app_identifier = cv @@ -1267,7 +1113,7 @@ impl OmikronConnection { async fn handle_app_challenge_response(self: Arc, cv: &CommunicationValue) { let sender_id = cv.get_sender(); if let Some((_, expected_challenge)) = self.app_challenges.remove(&sender_id) { - if let Some(DataValue::Str(response)) = cv.get_data(DataType::Challenge) { + if let DataValue::Str(response) = cv.get_data(DataType::Challenge) { if expected_challenge == *response { let res = CommunicationValue::new(CommunicationType::AppIdentificationResponse) .with_id(cv.get_id()) @@ -1602,6 +1448,301 @@ impl OmikronConnection { let _ = self.send_message(&live).await; } + async fn handle_message_send(self: Arc, cv: &CommunicationValue) { + let sender_id: u64 = cv.get_sender(); + + let receiver_id: i64 = if let Some(n) = cv.get_data(DataType::ReceiverId).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::ReceiverId).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + let timestamp_i64 = if let Some(n) = cv.get_data(DataType::SendTime).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::SendTime).as_str() { + s.parse::().unwrap_or_else(|_| now_millis_i64()) + } else { + now_millis_i64() + }; + let timestamp_u128 = timestamp_i64 as u128; + + let content = cv + .get_data(DataType::Content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; + let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); + + if let Some(reply_to) = reply_to { + match chat_files::get_message(sender_id as i64, reply_to, Some(receiver_id)) { + Ok(Some(_)) => {} + Ok(None) => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorNotFound)) + .await; + return; + } + Err(_) => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + } + } + + let is_local = iota_storage::users::user_manager::get_user(receiver_id).is_some(); + + if is_local { + chat_files::add_message( + timestamp_u128, + false, + receiver_id as i64, + sender_id as i64, + &content, + height, + reply_to, + ); + } + + chat_files::add_message( + timestamp_u128, + true, + sender_id as i64, + receiver_id as i64, + &content, + height, + reply_to, + ); + + let conf_msg = CommunicationValue::new(CommunicationType::MessageSend) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64); + let _ = self.send_message(&conf_msg).await; + + if !is_local { + self.forward_to_remote_iota( + cv, + sender_id as i64, + receiver_id, + timestamp_i64, + &content, + height, + reply_to, + ) + .await; + } else { + match self + .forward_message_live( + cv.get_id(), + receiver_id as u64, + sender_id as i64, + timestamp_i64, + &content, + height, + reply_to, + ) + .await + { + Some(ms) => { + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id, + sender_id as i64, + ms.clone(), + ); + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id, + ms.clone(), + ); + if is_read_receipts_enabled().await { + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp_i64 as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } + } + None => { + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id, + sender_id as i64, + MessageState::Sent, + ); + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id, + MessageState::Sent, + ); + let push_msg = CommunicationValue::new(CommunicationType::PushNotification) + .with_receiver(receiver_id as u64) + .add_typed_default( + DataType::SenderId, + DataValue::SignedNumber(sender_id as i128), + ); + let _ = self.send_message(&push_msg).await; + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp_i64 as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + } + } + } + + async fn handle_message_other_iota(self: Arc, cv: &CommunicationValue) { + let sender_id = &cv.get_sender(); + let receiver_id = &cv.get_receiver(); + + let timestamp = if let Some(n) = cv.get_data(DataType::SendTime).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::SendTime).as_str() { + s.parse::().unwrap_or_else(|_| now_millis_i64()) + } else { + now_millis_i64() + }; + + let content = cv + .get_data(DataType::Content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; + let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); + + chat_files::add_message( + timestamp as u128, + false, + *receiver_id as i64, + *sender_id as i64, + &content, + height, + reply_to, + ); + + match self + .forward_message_live( + cv.get_id(), + *receiver_id, + *sender_id as i64, + timestamp, + &content, + height, + reply_to, + ) + .await + { + Some(ms) => { + let _ = change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + ms.clone(), + ); + + if is_read_receipts_enabled().await { + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(*sender_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } + } + None => { + let _ = chat_files::change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + MessageState::Sent, + ); + + let push_msg = CommunicationValue::new(CommunicationType::PushNotification) + .with_receiver(*receiver_id) + .add_typed_default( + DataType::SenderId, + DataValue::SignedNumber(*sender_id as i128), + ); + let _ = self.send_message(&push_msg).await; + + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(*receiver_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + } + } + async fn handle_messages_get(self: Arc, cv: &CommunicationValue) { let _ = self .send_message(&message_handlers::handle_messages_get(cv)) @@ -2013,7 +2154,6 @@ impl OmikronClient for OmikronConnection { shutdown_tx: self.shutdown_tx.clone(), reconnect_on_close: self.reconnect_on_close.clone(), auth_failure: self.auth_failure.clone(), - keyring: self.keyring.clone(), app_challenges: self.app_challenges.clone(), app_sessions: self.app_sessions.clone(), handler_semaphore: self.handler_semaphore.clone(), @@ -2037,7 +2177,6 @@ impl OmikronClient for OmikronConnection { shutdown_tx: self.shutdown_tx.clone(), reconnect_on_close: self.reconnect_on_close.clone(), auth_failure: self.auth_failure.clone(), - keyring: self.keyring.clone(), app_challenges: self.app_challenges.clone(), app_sessions: self.app_sessions.clone(), handler_semaphore: self.handler_semaphore.clone(), diff --git a/omikron-connector/src/user_ops.rs b/omikron-connector/src/user_ops.rs index 3008120..241fd46 100644 --- a/omikron-connector/src/user_ops.rs +++ b/omikron-connector/src/user_ops.rs @@ -5,7 +5,6 @@ use iota_storage::users::user_profile::UserProfile; use iota_storage::util::config_util::CONFIG; use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64}; use iota_util::file_util::write_user_credential; -use iota_util::mtp_compat::OptionalDataValueExt; use iota_util::tu::TuCredential; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme}; @@ -34,9 +33,7 @@ pub enum LifecycleUserError { } impl From for LifecycleUserError { - fn from(value: crate::OmikronError) -> Self { - Self::Transport(value) - } + fn from(value: crate::OmikronError) -> Self { Self::Transport(value) } } fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec { @@ -49,31 +46,21 @@ fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> V } fn configured_iota_id() -> Result { - CONFIG - .load() - .iota_id + CONFIG.load().iota_id .and_then(|id| i64::try_from(id).ok()) .filter(|id| *id > 0) - .ok_or_else(|| { - LifecycleUserError::InvalidCredential("Iota identity is not registered".into()) - }) + .ok_or_else(|| LifecycleUserError::InvalidCredential("Iota identity is not registered".into())) } -fn sign_lifecycle_payload( - credential: &TuCredential, - payload: &[u8], -) -> Result<(Vec, Vec), LifecycleUserError> { +fn sign_lifecycle_payload(credential: &TuCredential, payload: &[u8]) -> Result<(Vec, Vec), LifecycleUserError> { let classical = Ed25519Signer::new(&credential.keyring.sig_cl_secret_key) .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))? .sign(payload) .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; - let pq = MlDsaSigner::new( - &credential.keyring.sig_pq_secret_key, - &credential.keyring.sig_pq_public_key, - ) - .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))? - .sign(payload) - .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; + let pq = MlDsaSigner::new(&credential.keyring.sig_pq_secret_key, &credential.keyring.sig_pq_public_key) + .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))? + .sign(payload) + .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; Ok((classical, pq)) } @@ -84,25 +71,15 @@ async fn inspect_credential_account( if credential.omega_host != omega_discovery::omega_host() { return Err(LifecycleUserError::OmegaHostMismatch); } - let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( - DataType::UserId, - DataValue::SignedNumber(credential.user_id.into()), - ); - let response = connection - .await_response(&request, Duration::from_secs(20)) - .await?; + let request = CommunicationValue::new(CommunicationType::GetUserData) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into())); + let response = connection.await_response(&request, Duration::from_secs(20)).await?; if !response.is_type(CommunicationType::GetUserData) { return Err(LifecycleUserError::RemoteRejected); } - let username = response - .get_data(DataType::Username) - .as_str() - .map(str::to_owned) + let username = response.get_data(DataType::Username).as_str().map(str::to_owned) .ok_or(LifecycleUserError::RemoteRejected)?; - let public_key = response - .get_data(DataType::PublicKey) - .as_str() - .map(str::to_owned) + let public_key = response.get_data(DataType::PublicKey).as_str().map(str::to_owned) .ok_or(LifecycleUserError::RemoteRejected)?; if public_key != public_key_bundle_to_base64(&credential.public_key_bundle()) { return Err(LifecycleUserError::RemoteRejected); @@ -119,121 +96,54 @@ async fn credential_proof( domain: &[u8], ) -> Result<(), LifecycleUserError> { let iota_id = configured_iota_id()?; - let begin_request = CommunicationValue::new(begin).add_typed_default( - DataType::UserId, - DataValue::SignedNumber(credential.user_id.into()), - ); - let challenge_response = connection - .await_response(&begin_request, Duration::from_secs(20)) - .await?; + let begin_request = CommunicationValue::new(begin) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into())); + let challenge_response = connection.await_response(&begin_request, Duration::from_secs(20)).await?; if !challenge_response.is_type(challenge) { return Err(LifecycleUserError::RemoteRejected); } - let nonce = challenge_response - .get_data(DataType::ServerNonce) - .as_signed_number() + let nonce = challenge_response.get_data(DataType::ServerNonce).as_signed_number() .and_then(|value| u64::try_from(value).ok()) .ok_or(LifecycleUserError::RemoteRejected)?; - let (signature, pq_signature) = sign_lifecycle_payload( - credential, - &lifecycle_payload(domain, credential.user_id, iota_id, nonce), - )?; + let (signature, pq_signature) = sign_lifecycle_payload(credential, &lifecycle_payload(domain, credential.user_id, iota_id, nonce))?; let complete_request = CommunicationValue::new(complete) - .add_typed_default( - DataType::UserId, - DataValue::SignedNumber(credential.user_id.into()), - ) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into())) .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())) .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) .add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); - let response = connection - .await_response(&complete_request, Duration::from_secs(20)) - .await?; - if response.is_type(CommunicationType::Success) { - Ok(()) - } else { - Err(LifecycleUserError::RemoteRejected) - } + let response = connection.await_response(&complete_request, Duration::from_secs(20)).await?; + if response.is_type(CommunicationType::Success) { Ok(()) } else { Err(LifecycleUserError::RemoteRejected) } } /// Attach or migrate an existing account. Local state is written only after /// Omega has accepted the credential proof and changed its assignment. -pub async fn attach_user_from_tu( - connection: &dyn OmikronClient, - contents: &str, -) -> Result { - let credential = TuCredential::parse(contents) - .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; +pub async fn attach_user_from_tu(connection: &dyn OmikronClient, contents: &str) -> Result { + let credential = TuCredential::parse(contents).map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; let (username, public_key) = inspect_credential_account(connection, &credential).await?; - credential_proof( - connection, - &credential, - CommunicationType::AttachUserBegin, - CommunicationType::AttachUserChallenge, - CommunicationType::AttachUserComplete, - b"tensamin:user-attach:v1\0", - ) - .await?; - let profile = UserProfile::new( - credential.user_id, - username, - None, - public_key, - hex_hash(contents), - String::new(), - ); + credential_proof(connection, &credential, CommunicationType::AttachUserBegin, CommunicationType::AttachUserChallenge, CommunicationType::AttachUserComplete, b"tensamin:user-attach:v1\0").await?; + let profile = UserProfile::new(credential.user_id, username, None, public_key, hex_hash(contents), String::new()); write_user_credential(profile.user_id, &credential.to_canonical_string()) .map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; - try_add_user(profile.clone()) - .map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; + try_add_user(profile.clone()).map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; Ok(profile) } -pub async fn complete_delete_user_with_tu( - connection: &dyn OmikronClient, - contents: &str, - expected_user_id: i64, -) -> Result<(), LifecycleUserError> { - let credential = TuCredential::parse(contents) - .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; - if credential.user_id != expected_user_id { - return Err(LifecycleUserError::InvalidCredential( - "credential user ID does not match deletion target".into(), - )); - } +pub async fn complete_delete_user_with_tu(connection: &dyn OmikronClient, contents: &str, expected_user_id: i64) -> Result<(), LifecycleUserError> { + let credential = TuCredential::parse(contents).map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; + if credential.user_id != expected_user_id { return Err(LifecycleUserError::InvalidCredential("credential user ID does not match deletion target".into())); } inspect_credential_account(connection, &credential).await?; - credential_proof( - connection, - &credential, - CommunicationType::DeleteUserCredentialBegin, - CommunicationType::DeleteUserCredentialChallenge, - CommunicationType::DeleteUserCredentialComplete, - b"tensamin:user-delete:v1\0", - ) - .await + credential_proof(connection, &credential, CommunicationType::DeleteUserCredentialBegin, CommunicationType::DeleteUserCredentialChallenge, CommunicationType::DeleteUserCredentialComplete, b"tensamin:user-delete:v1\0").await } /// Repair local management state after a release or migration committed in /// Omega but local cleanup was interrupted. Hosted data is retained. pub async fn reconcile_managed_users(connection: &dyn OmikronClient) { - let Ok(local_iota_id) = configured_iota_id() else { - return; - }; + let Ok(local_iota_id) = configured_iota_id() else { return; }; for user in iota_storage::users::user_manager::get_users() { - let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( - DataType::UserId, - DataValue::SignedNumber(user.user_id.into()), - ); - let Ok(response) = connection - .await_response(&request, Duration::from_secs(10)) - .await - else { - continue; - }; - let remote_iota_id = response - .get_data(DataType::IotaId) - .as_signed_number() - .and_then(|value| i64::try_from(value).ok()); + let request = CommunicationValue::new(CommunicationType::GetUserData) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(user.user_id.into())); + let Ok(response) = connection.await_response(&request, Duration::from_secs(10)).await else { continue; }; + let remote_iota_id = response.get_data(DataType::IotaId).as_signed_number().and_then(|value| i64::try_from(value).ok()); if remote_iota_id != Some(local_iota_id) { let _ = iota_storage::users::user_manager::release_user(user.user_id); } @@ -358,12 +268,7 @@ pub async fn create_user( log!("Created User"); write_user_credential( user_id, - &format!( - "{}@{}::{}", - user_id, - omega_discovery::omega_host(), - keyring_b64 - ), + &format!("{}@{}::{}", user_id, omega_discovery::omega_host(), keyring_b64), ) .map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?; @@ -377,7 +282,6 @@ mod tests { use super::{CreateUserError, request_user_id, valid_username}; use crate::{OmikronClient, OmikronError}; use async_trait::async_trait; - use iota_util::mtp_compat::CommunicationValueCompat; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use std::time::Duration; diff --git a/web-server/Cargo.toml b/web-server/Cargo.toml index ad49fba..be6b65a 100644 --- a/web-server/Cargo.toml +++ b/web-server/Cargo.toml @@ -5,7 +5,7 @@ version = "0.1.0" edition = "2024" [dependencies] -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] } bytes = "1" http = "1" iota-logger = { path = "../iota-logger" }