From 326ebf3b3728fded062193b6b74b6f7e17432a9e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 8 Aug 2026 02:09:38 +0200 Subject: [PATCH 1/3] [Fix] Replies --- Cargo.lock | 320 +++++++++++--------- client/src/client_connection.rs | 6 + iota-connection/src/message_handlers.rs | 36 ++- iota-storage/src/util/chat_files.rs | 56 ++++ omikron-connector/src/omikron_connection.rs | 25 ++ 5 files changed, 303 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 149db71..c8e129d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,16 +44,16 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.13.1" +version = "3.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" +checksum = "53200bd1513e569e6e644181c922cec072c121a27db5b38d45e88e630c369366" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-tls", "actix-utils", - "base64", + "base64 0.22.1", "bitflags 2.13.1", "brotli", "bytes", @@ -119,9 +119,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +checksum = "3716aae056e2f869b7b5cfd8a08fcf98890f8455bec61d69c7dff5d8576f9d2b" dependencies = [ "actix-rt", "actix-service", @@ -129,7 +129,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.10", + "socket2", "tokio", "tracing", ] @@ -211,7 +211,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.5", + "socket2", "time", "tracing", "url", @@ -290,9 +290,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -320,9 +320,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -483,9 +483,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -494,9 +494,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -511,6 +511,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -651,9 +657,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "9066c49992464636f92905fa096ec58baaa4d57ec19a5c096c68d3e25ef3d136" dependencies = [ "find-msvc-tools", "jobserver", @@ -780,7 +786,7 @@ dependencies = [ "actix-web-actors", "aes-gcm", "async-trait", - "base64", + "base64 0.22.1", "chrono", "crossterm", "dashmap", @@ -1089,6 +1095,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek-derive" version = "0.1.1" @@ -1102,9 +1124,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" dependencies = [ "darling_core", "darling_macro", @@ -1112,26 +1134,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" dependencies = [ "darling_core", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1150,9 +1172,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "deflate64" @@ -1173,7 +1195,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", - "pem-rfc7468", "zeroize", ] @@ -1184,6 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid 0.10.2", + "pem-rfc7468", "zeroize", ] @@ -1310,20 +1332,45 @@ dependencies = [ "signature 2.2.0", ] +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8 0.11.0", + "signature 3.0.0", +] + [[package]] name = "ed25519-dalek" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", "serde", "sha2 0.10.9", "subtle", "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", + "serde", + "sha2 0.11.0", + "signature 3.0.0", + "subtle", + "zeroize", +] + [[package]] name = "ed448-goldilocks" version = "0.7.2" @@ -1397,12 +1444,6 @@ dependencies = [ "regex", ] -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - [[package]] name = "fastbloom" version = "0.17.0" @@ -1433,6 +1474,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "filedescriptor" version = "0.8.3" @@ -1446,9 +1493,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "finl_unicode" @@ -1698,7 +1745,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.2", + "http 1.5.0", "indexmap", "slab", "tokio", @@ -1715,7 +1762,7 @@ dependencies = [ "bytes", "fastrand", "futures-util", - "http 1.4.2", + "http 1.5.0", "pin-project-lite", "tokio", ] @@ -1756,7 +1803,7 @@ dependencies = [ "futures-util", "h3", "h3-datagram", - "http 1.4.2", + "http 1.5.0", "pin-project-lite", "tokio", "tracing", @@ -1805,10 +1852,10 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "headers-core", - "http 1.4.2", + "http 1.5.0", "httpdate", "mime", "sha1 0.10.7", @@ -1820,7 +1867,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -1890,9 +1937,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1905,7 +1952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -1916,7 +1963,7 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", - "http 1.4.2", + "http 1.5.0", "http-body", "pin-project-lite", ] @@ -1935,9 +1982,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "ctutils", "typenum", @@ -1954,7 +2001,7 @@ dependencies = [ "futures-channel", "futures-core", "h2 0.4.15", - "http 1.4.2", + "http 1.5.0", "http-body", "httparse", "httpdate", @@ -1971,7 +2018,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.2", + "http 1.5.0", "hyper", "hyper-util", "rustls", @@ -1986,18 +2033,18 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2", "system-configuration", "tokio", "tower-service", @@ -2180,15 +2227,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ "darling", "indoc", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -2215,7 +2262,7 @@ version = "0.1.0" dependencies = [ "aes-gcm", "async-trait", - "base64", + "base64 0.22.1", "chrono", "crossterm", "dashmap", @@ -2265,7 +2312,7 @@ dependencies = [ "actix-web-actors", "aes-gcm", "async-trait", - "base64", + "base64 0.22.1", "chrono", "crossterm", "dashmap", @@ -2457,7 +2504,7 @@ version = "0.1.0" dependencies = [ "aes-gcm", "arc-swap", - "base64", + "base64 0.22.1", "hex", "hkdf 0.12.4", "iota-logger", @@ -2502,8 +2549,8 @@ version = "0.1.0" dependencies = [ "aes-gcm", "anyhow", - "base64", - "ed25519-dalek", + "base64 0.22.1", + "ed25519-dalek 2.2.0", "hex", "hkdf 0.12.4", "iota-logger", @@ -2532,7 +2579,7 @@ dependencies = [ name = "iota-util" version = "0.1.0" dependencies = [ - "base64", + "base64 0.22.1", "hex", "iota-paths", "mtp", @@ -2546,9 +2593,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "ipnetwork" @@ -2697,9 +2744,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -2753,9 +2800,9 @@ dependencies = [ [[package]] name = "line-clipping" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ "bitflags 2.13.1", ] @@ -2812,9 +2859,9 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" dependencies = [ "hashbrown 0.17.1", ] @@ -2966,7 +3013,7 @@ dependencies = [ [[package]] name = "mtp" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-client", "mtp-codec", @@ -2982,7 +3029,7 @@ dependencies = [ [[package]] name = "mtp-client" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-codec", "mtp-common", @@ -2995,9 +3042,9 @@ dependencies = [ [[package]] name = "mtp-codec" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ - "base64", + "base64 0.23.1", "byteorder", "mtp-common", "mtp-crypto", @@ -3008,7 +3055,7 @@ dependencies = [ [[package]] name = "mtp-common" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "quinn", "rustls", @@ -3019,11 +3066,11 @@ dependencies = [ [[package]] name = "mtp-crypto" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ - "base64", + "base64 0.23.1", "chacha20poly1305", - "ed25519-dalek", + "ed25519-dalek 3.0.0", "getrandom 0.4.3", "hkdf 0.13.0", "ml-dsa", @@ -3033,7 +3080,7 @@ dependencies = [ "rustls", "serde", "sha2 0.11.0", - "thiserror 1.0.69", + "thiserror 2.0.19", "tokio", "zeroize", ] @@ -3041,24 +3088,24 @@ dependencies = [ [[package]] name = "mtp-files" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-crypto", "rand 0.10.2", - "thiserror 1.0.69", + "thiserror 2.0.19", "zeroize", ] [[package]] name = "mtp-host" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-codec", "mtp-common", "mtp-crypto", "mtp-transport", - "rand 0.8.7", + "rand 0.10.2", "tokio", "tracing", "wtransport", @@ -3067,7 +3114,7 @@ dependencies = [ [[package]] name = "mtp-transport" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "async-trait", "mtp-codec", @@ -3085,7 +3132,7 @@ dependencies = [ [[package]] name = "mtp-type-map" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "serde", "serde_yaml", @@ -3094,14 +3141,14 @@ dependencies = [ [[package]] name = "mtp-webserver" version = "0.2.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "async-trait", "bytes", "h3", "h3-quinn", "h3-webtransport", - "http 1.4.2", + "http 1.5.0", "http-body-util", "hyper", "hyper-util", @@ -3306,7 +3353,7 @@ name = "omikron-connector" version = "0.1.0" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "dashmap", "hex", "iota-connection", @@ -3414,7 +3461,7 @@ dependencies = [ "actix-web-actors", "aes-gcm", "async-trait", - "base64", + "base64 0.22.1", "chrono", "crossterm", "dashmap", @@ -3459,21 +3506,21 @@ dependencies = [ [[package]] name = "palette" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" dependencies = [ "approx", - "fast-srgb8", "libm", "palette_derive", + "palette_math", ] [[package]] name = "palette_derive" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" dependencies = [ "by_address", "proc-macro2", @@ -3481,6 +3528,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -3520,15 +3576,15 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] [[package]] name = "pem-rfc7468" -version = "0.7.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] @@ -3858,7 +3914,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.5", + "socket2", "thiserror 2.0.19", "tokio", "tracing", @@ -3899,7 +3955,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2", "tracing", "windows-sys 0.61.2", ] @@ -4137,9 +4193,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4164,12 +4220,12 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "h2 0.4.15", - "http 1.4.2", + "http 1.5.0", "http-body", "http-body-util", "hyper", @@ -4567,7 +4623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ "digest 0.11.3", - "keccak 0.2.0", + "keccak 0.2.1", "sponge-cursor", ] @@ -4667,16 +4723,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.5" @@ -4913,7 +4959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "bitflags 2.13.1", "fancy-regex", "filedescriptor", @@ -4990,9 +5036,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -5057,20 +5103,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.5", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5157,7 +5203,7 @@ dependencies = [ "bitflags 2.13.1", "bytes", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body", "pin-project-lite", "tower", @@ -5224,7 +5270,7 @@ checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ "bytes", "data-encoding", - "http 1.4.2", + "http 1.5.0", "httparse", "log", "native-tls", @@ -5399,7 +5445,7 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.4.2", + "http 1.5.0", "http-body", "http-body-util", "log", @@ -5492,7 +5538,7 @@ name = "web-server" version = "0.1.0" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", "iota-logger", "iota-util", "mtp", @@ -5528,7 +5574,7 @@ dependencies = [ "actix-web-actors", "aes-gcm", "async-trait", - "base64", + "base64 0.22.1", "chrono", "crossterm", "dashmap", @@ -5912,7 +5958,7 @@ dependencies = [ "rustls-native-certs", "rustls-pki-types", "sha2 0.11.0", - "socket2 0.6.5", + "socket2", "thiserror 2.0.19", "time", "tokio", @@ -5940,7 +5986,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", "serde", "zeroize", @@ -6011,18 +6057,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -6132,9 +6178,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/client/src/client_connection.rs b/client/src/client_connection.rs index cda5cf8..512cc31 100644 --- a/client/src/client_connection.rs +++ b/client/src/client_connection.rs @@ -407,6 +407,12 @@ impl ClientConnection { return; } + if cv.is_type(CommunicationType::MessageGet) { + self.send_message(&message_handlers::handle_message_get(&cv)) + .await; + return; + } + if cv.is_type(CommunicationType::GetChats) { self.send_message(&message_handlers::handle_get_chats(&cv)) .await; diff --git a/iota-connection/src/message_handlers.rs b/iota-connection/src/message_handlers.rs index 73244d7..dc1d4dc 100644 --- a/iota-connection/src/message_handlers.rs +++ b/iota-connection/src/message_handlers.rs @@ -102,11 +102,11 @@ pub fn handle_message_delete(cv: &CommunicationValue) -> CommunicationValue { } } -fn stored_message_value( +fn stored_message_fields( message: &chat_files::StoredMessage, storage_owner: i64, partner_id: i64, -) -> DataValue { +) -> Vec<(DataType, DataValue)> { let mut fields = vec![ ( DataType::MessageId, @@ -162,7 +162,15 @@ fn stored_message_value( .collect(); fields.push((DataType::Reactions, DataValue::Array(reactions))); } - typed_container(fields) + fields +} + +fn stored_message_value( + message: &chat_files::StoredMessage, + storage_owner: i64, + partner_id: i64, +) -> DataValue { + typed_container(stored_message_fields(message, storage_owner, partner_id)) } pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue { @@ -520,6 +528,28 @@ pub fn handle_messages_get(cv: &CommunicationValue) -> CommunicationValue { .add_typed_default(DataType::Messages, DataValue::Array(msg_array)) } +pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue { + let Some(send_time) = data_i64(cv, DataType::SendTime) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let partner_id = data_i64(cv, DataType::ChatPartnerId); + let owner = cv.get_sender() as i64; + + let message = match chat_files::get_message(owner, send_time, partner_id) { + Ok(Some(message)) => message, + Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound), + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + + let mut response = CommunicationValue::new(CommunicationType::MessageGet) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()); + for (data_type, value) in stored_message_fields(&message, owner, message.external_user) { + response = response.add_typed_default(data_type, value); + } + response +} + pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue { let user_id = cv.get_sender(); let users = chats_util::get_users(user_id as i64); diff --git a/iota-storage/src/util/chat_files.rs b/iota-storage/src/util/chat_files.rs index da05aad..024c302 100644 --- a/iota-storage/src/util/chat_files.rs +++ b/iota-storage/src/util/chat_files.rs @@ -644,6 +644,62 @@ pub fn get_messages( } } +pub fn get_message( + storage_owner: i64, + message_time: i64, + external_user: Option, +) -> Result, StorageError> { + db::with_db(|conn| { + let mut stmt = conn.prepare( + r#" + SELECT id, message_time, content, sent_by_self, message_state, height, + reply_to, edited_count, external_user + FROM messages + WHERE storage_owner = ?1 + AND message_time = ?2 + AND deleted_by_external = 0 + AND (?3 IS NULL OR external_user = ?3) + ORDER BY id DESC + "#, + )?; + + let rows = stmt.query_map(params![storage_owner, message_time, external_user], |row| { + Ok(StoredMessage { + id: row.get(0)?, + message_time: row.get(1)?, + content: row.get(2)?, + sent_by_self: row.get::<_, i64>(3)? != 0, + message_state: row.get(4)?, + height: row.get(5).unwrap_or(0), + reply_to: row.get(6).ok().flatten(), + edited: row.get::<_, i64>(7).unwrap_or(0) > 0, + external_user: row.get(8)?, + reactions: Vec::new(), + }) + })?; + + let messages: Vec = rows.collect::>()?; + if messages.is_empty() { + return Ok(None); + } + if external_user.is_none() + && messages + .iter() + .map(|message| message.external_user) + .collect::>() + .len() + > 1 + { + return Ok(None); + } + + let mut message = messages.into_iter().next().expect("checked non-empty"); + let reaction_map = load_reactions(conn, &[message.id]); + message.reactions = reaction_map.get(&message.id).cloned().unwrap_or_default(); + Ok(Some(message)) + }) +} + pub fn get_messages_by_ids(storage_owner: i64, ids: &[i64]) -> Vec { if ids.is_empty() { return Vec::new(); diff --git a/omikron-connector/src/omikron_connection.rs b/omikron-connector/src/omikron_connection.rs index 53da661..81d6ae6 100755 --- a/omikron-connector/src/omikron_connection.rs +++ b/omikron-connector/src/omikron_connection.rs @@ -884,6 +884,7 @@ impl OmikronConnection { 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); dispatch!(AddConversation, handle_add_conversation); @@ -1447,6 +1448,24 @@ impl OmikronConnection { 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 { @@ -1701,6 +1720,12 @@ impl OmikronConnection { .await; } + async fn handle_message_get(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_message_get(cv)) + .await; + } + async fn handle_get_chats(self: Arc, cv: &CommunicationValue) { let _ = self .send_message(&message_handlers::handle_get_chats(cv)) From 7dc98ef29b3c428c20b777295e93fdea8a21a214 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 9 Aug 2026 02:51:47 +0200 Subject: [PATCH 2/3] [Fix] User deletion & migration --- Cargo.lock | 1 + iota-cli/src/ipc_client.rs | 3 + iota-cli/src/screens/users.rs | 32 +++-- iota-cli/src/ui.rs | 3 + iota-daemon-lib/src/command_router.rs | 96 +++++++++---- iota-daemon/src/main.rs | 17 +++ iota-ipc/src/lib.rs | 2 +- iota-ipc/src/protocol.rs | 42 ++++++ iota-storage/src/users/user_manager.rs | 136 +++++++++++++----- iota-storage/src/users/user_profile.rs | 10 +- iota-storage/src/util/db.rs | 21 ++- iota-storage/src/util/e2ee_storage.rs | 17 +++ iota-util/src/file_util.rs | 63 ++++++++- iota-util/src/lib.rs | 1 + iota-util/src/tu.rs | 95 +++++++++++++ iota/Cargo.toml | 1 + iota/src/cli_args.rs | 106 +++++++++----- iota/src/main.rs | 52 ++++++- omikron-connector/src/omikron_connection.rs | 29 ++++ omikron-connector/src/user_ops.rs | 144 ++++++++++++++++++-- 20 files changed, 742 insertions(+), 129 deletions(-) create mode 100644 iota-util/src/tu.rs diff --git a/Cargo.lock b/Cargo.lock index c8e129d..1003ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2250,6 +2250,7 @@ dependencies = [ "iota-paths", "iota-process-manager", "iota-terms", + "iota-util", "serde_json", "serde_yaml", "tokio", diff --git a/iota-cli/src/ipc_client.rs b/iota-cli/src/ipc_client.rs index ad0796f..ca912fc 100644 --- a/iota-cli/src/ipc_client.rs +++ b/iota-cli/src/ipc_client.rs @@ -483,6 +483,9 @@ impl IpcClient { ResponsePayload::UserRemoved { user_id } => { format!("Removed user {}", user_id) } + ResponsePayload::UserDataPurged { user_id } => { + format!("Purged hosted data for {}", user_id) + } ResponsePayload::Acknowledged { message } => message.clone(), ResponsePayload::DaemonStatus(status) => status.formatted.clone(), ResponsePayload::Config(config) => config.yaml.clone(), diff --git a/iota-cli/src/screens/users.rs b/iota-cli/src/screens/users.rs index fab710c..93d4b13 100644 --- a/iota-cli/src/screens/users.rs +++ b/iota-cli/src/screens/users.rs @@ -27,6 +27,9 @@ use std::{ pub struct UserEntry { pub user_id: i64, pub username: String, + pub state: iota_ipc::LocalUserState, + pub data_present: bool, + pub credential_present: bool, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -146,7 +149,9 @@ impl UsersScreen { let user = &self.users[*user_index]; ( *user_index, - format!("{:>6} {}", user.user_id, user.username), + 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(); @@ -211,7 +216,7 @@ impl UsersScreen { f, buttons_area[2], ActionButton { - label: "Remove", + label: "Release", intent: ButtonIntent::Destructive, focused: self.focus == Focus::RemoveButton, enabled: !self.loading && !self.pending && !self.users.is_empty(), @@ -235,7 +240,7 @@ impl UsersScreen { return InteractionResult::AppTask { task: Box::pin(async move { let result = match ipc.send_request(iota_ipc::LocalRequest::CreateUser { username: name }).await { - Ok(iota_ipc::ResponseResult::Ok(iota_ipc::ResponsePayload::UserCreated { user_id, username })) => Ok(UserEntry { user_id, username }), + Ok(iota_ipc::ResponseResult::Ok(iota_ipc::ResponsePayload::UserCreated { user_id, username })) => Ok(UserEntry { user_id, username, state: iota_ipc::LocalUserState::Managed, data_present: true, credential_present: true }), Ok(iota_ipc::ResponseResult::Error(error)) => Err(format!("Cannot create user: {error}")), Ok(_) => Err("Daemon returned an unexpected response while creating the user.".into()), Err(error) => Err(format!("Cannot create user: {error}")), @@ -249,14 +254,14 @@ impl UsersScreen { let ipc = self.ipc.clone(); let id = user.user_id; self.pending = true; - self.message = Some(format!("Removing {}…", user.username)); + self.message = Some(format!("Releasing {}…", user.username)); return InteractionResult::AppTask { task: Box::pin(async move { - let result = match ipc.send_request(iota_ipc::LocalRequest::RemoveUser { user_id: id }).await { - Ok(iota_ipc::ResponseResult::Ok(iota_ipc::ResponsePayload::UserRemoved { .. })) => Ok(()), - Ok(iota_ipc::ResponseResult::Error(error)) => Err(format!("Cannot remove user: {error}")), - Ok(_) => Err("Daemon returned an unexpected response while removing the user.".into()), - Err(error) => Err(format!("Cannot remove user: {error}")), + let result = match ipc.send_request(iota_ipc::LocalRequest::ReleaseUser { user_id: id }).await { + Ok(iota_ipc::ResponseResult::Ok(iota_ipc::ResponsePayload::Acknowledged { .. })) => Ok(()), + Ok(iota_ipc::ResponseResult::Error(error)) => Err(format!("Cannot release user: {error}")), + Ok(_) => Err("Daemon returned an unexpected response while releasing the user.".into()), + Err(error) => Err(format!("Cannot release user: {error}")), }; UiEvent::App(AppEvent::UserRemoved { user_id: id, @@ -528,10 +533,11 @@ impl Screen for UsersScreen { match result { Ok(()) => { self.pending_dialog = None; - self.users.retain(|user| user.user_id != user_id); - self.focused_index = - self.focused_index.min(self.users.len().saturating_sub(1)); - self.message = Some(format!("Removed user {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.")); } Err(error) => { self.dialog = self.pending_dialog.take(); diff --git a/iota-cli/src/ui.rs b/iota-cli/src/ui.rs index b06dab1..dad4c80 100644 --- a/iota-cli/src/ui.rs +++ b/iota-cli/src/ui.rs @@ -602,6 +602,9 @@ impl UI { .map(|u| UserEntry { user_id: u.user_id, username: u.username, + state: u.state, + data_present: u.data_present, + credential_present: u.credential_present, }) .collect()) } diff --git a/iota-daemon-lib/src/command_router.rs b/iota-daemon-lib/src/command_router.rs index 58e627d..74f5f96 100644 --- a/iota-daemon-lib/src/command_router.rs +++ b/iota-daemon-lib/src/command_router.rs @@ -9,8 +9,9 @@ use iota_ipc::{ use iota_logger::{log, log_command}; use iota_storage::users::user_manager; use iota_storage::util::config_util::{self}; -use mtp::codec::{CommunicationType, CommunicationValue}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use crate::daemon_state::{ShutdownReason, StartupPhase}; @@ -51,7 +52,10 @@ impl CommandRouter { } let needs_omikron = matches!( request, - LocalRequest::CreateUser { .. } | LocalRequest::RemoveUser { .. } + LocalRequest::CreateUser { .. } + | LocalRequest::AttachUserFromTu { .. } + | LocalRequest::ReleaseUser { .. } + | LocalRequest::CompleteDeleteUser { .. } ); if needs_omikron && !self.services.omikron.is_connected().await { return ResponseResult::Error( @@ -92,11 +96,15 @@ impl CommandRouter { ResponseResult::Ok(ResponsePayload::Tasks(tasks)) } LocalRequest::ListUsers => { - let users: Vec = user_manager::get_users() + let users: Vec = user_manager::get_residency() .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_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 }, + data_present: user.data_present, }) .collect(); ResponseResult::Ok(ResponsePayload::Users(users)) @@ -137,18 +145,61 @@ impl CommandRouter { } } } - LocalRequest::RemoveUser { user_id } => { - let user = match user_manager::get_user(user_id) { - Some(user) => user, - None => return ResponseResult::Error(IpcErrorCode::NotFound), + 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}"); + ResponseResult::Error(IpcErrorCode::StorageFailure) + } + }, + 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) }), + Err(error) => { + log!("Credential attach failed: {error:?}"); + ResponseResult::Error(IpcErrorCode::Unauthorized) + } + } + } + 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(|value| value.ok_or(())), }; - let message = CommunicationValue::new(CommunicationType::DeleteUser) - .with_sender(user.user_id as u64); - if let Err(_e) = self.services.omikron.send_message(&message).await { - return ResponseResult::Error(IpcErrorCode::OmikronUnavailable); + 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) + } + } + } + LocalRequest::RemoveUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), + LocalRequest::ReleaseUser { user_id } => { + if user_manager::get_user(user_id).is_none() { + return ResponseResult::Error(IpcErrorCode::NotFound); + } + 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) + } + }, + 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(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable), } - user_manager::remove_user(user.user_id); - ResponseResult::Ok(ResponsePayload::UserRemoved { user_id }) } LocalRequest::ReconnectOmikron => match self.services.omikron.reconnect().await { Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { @@ -242,23 +293,22 @@ impl CommandRouter { ResponseResult::Ok(ResponsePayload::Components(components)) } LocalRequest::GetUser { user_id } => match user_manager::get_user(user_id) { - Some(user) => ResponseResult::Ok(ResponsePayload::UserDetail(UserDetailResponse { + Some(user) => { + 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, + })) + }, None => ResponseResult::Error(IpcErrorCode::NotFound), }, - LocalRequest::ImportUser { username } => { - match user_manager::load_from_tu(&username).await { - Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { - message: format!("Imported user {username}"), - }), - Err(()) => ResponseResult::Error(IpcErrorCode::StorageFailure), - } - } + LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), LocalRequest::GetLogs { limit } => { let entries = if let Ok(buf) = self.log_buffer.lock() { buf.recent(limit) diff --git a/iota-daemon/src/main.rs b/iota-daemon/src/main.rs index 47a0a51..76eeaf0 100644 --- a/iota-daemon/src/main.rs +++ b/iota-daemon/src/main.rs @@ -156,6 +156,7 @@ async fn main() -> ExitCode { } }; let omikron_health = omikron.clone(); + let omikron_reconcile = omikron.clone(); let services = DaemonServices::new(omikron); let health_runtime = runtime.clone(); runtime @@ -209,6 +210,22 @@ async fn main() -> ExitCode { Ok(()) }) .await; + runtime + .tasks + .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; + } + tokio::select! { + changed = states.changed() => if changed.is_err() { break }, + _ = tokio::time::sleep(Duration::from_secs(30)) => {}, + } + } + Ok(()) + }) + .await; let ipc_server = match IpcServer::bind( socket.clone(), runtime.clone(), diff --git a/iota-ipc/src/lib.rs b/iota-ipc/src/lib.rs index 0ff6284..bd3d102 100644 --- a/iota-ipc/src/lib.rs +++ b/iota-ipc/src/lib.rs @@ -8,7 +8,7 @@ pub use protocol::{ ExitIntent, HealthStatus, HelloAck, IpcErrorCode, LifecycleEvent, LifecyclePhase, LocalRequest, LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse, RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult, StartupPhase, StateSnapshot, StatusResponse, - SupervisorKind, TaskSummary, UpdateStatusResponse, UserDetailResponse, UserSummary, + 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 d31bee4..2e2be1a 100644 --- a/iota-ipc/src/protocol.rs +++ b/iota-ipc/src/protocol.rs @@ -1,5 +1,17 @@ use serde::{Deserialize, Serialize}; +/// IPC credentials are supplied by the interactive CLI, never a daemon-side +/// path lookup. Debug is deliberately redacted because command routing logs +/// the request value. +#[derive(Clone, Deserialize, Serialize)] +pub struct SecretString(pub String); + +impl std::fmt::Debug for SecretString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + // --------------------------------------------------------------------------- // Client → Daemon // --------------------------------------------------------------------------- @@ -36,6 +48,21 @@ pub enum LocalRequest { CreateUser { username: String, }, + AttachUserFromTu { + credential: SecretString, + }, + PurgeUserData { + user_id: i64, + }, + ReleaseUser { + user_id: i64, + }, + CompleteDeleteUser { + user_id: i64, + credential: Option, + }, + /// Retained only to return an actionable deprecation error to old IPC + /// clients. It must never select lifecycle semantics implicitly. RemoveUser { user_id: i64, }, @@ -137,7 +164,9 @@ pub enum ResponsePayload { Tasks(Vec), Users(Vec), 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 }, DaemonStatus(DaemonStatusResponse), Config(ConfigResponse), @@ -174,6 +203,9 @@ pub struct UserDetailResponse { pub display_name: Option, pub created_at: i64, pub trusted_apps: Vec, + pub state: LocalUserState, + pub data_present: bool, + pub credential_present: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -208,6 +240,16 @@ pub struct TaskSummary { pub struct UserSummary { pub user_id: i64, pub username: String, + pub state: LocalUserState, + pub data_present: bool, + pub credential_present: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum LocalUserState { + Managed, + Released, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/iota-storage/src/users/user_manager.rs b/iota-storage/src/users/user_manager.rs index af842e5..ccd3c9e 100644 --- a/iota-storage/src/users/user_manager.rs +++ b/iota-storage/src/users/user_manager.rs @@ -1,10 +1,26 @@ use crate::users::user_profile::UserProfile; use crate::util::db; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use iota_util::crypto_helper::{self, hex_hash, keyring_from_base64, public_key_bundle_to_base64}; -use iota_util::file_util::{load_file, save_file}; -use rand_core::{OsRng, RngCore}; +use iota_util::file_util::{delete_user_directory, load_file, remove_user_credential, save_file}; use rusqlite::params; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LocalUserState { + Managed, + Released, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UserResidency { + pub user_id: i64, + pub username: String, + pub state: LocalUserState, + pub data_present: bool, +} + +fn now_millis() -> i64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as i64 +} pub fn add_user(user: UserProfile) { if let Err(e) = try_add_user(user) { @@ -45,6 +61,12 @@ pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::Stora params![user.user_id, app_id, app_secret], )?; } + conn.execute( + r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at) + VALUES (?1, ?2, 'managed', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3) + ON CONFLICT(user_id) DO UPDATE SET username = excluded.username, lifecycle_state = 'managed', updated_at = excluded.updated_at"#, + params![user.user_id, user.username, now_millis()], + )?; Ok(()) }) } @@ -205,42 +227,92 @@ pub fn remove_user(user_id: i64) { } } +/// Remove only local management authority. Hosted content is intentionally +/// retained and is indexed as released for a later purge operation. +pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageError> { + let username = get_user(user_id).map(|user| user.username).ok_or_else(|| { + crate::storage_error::StorageError::Other("managed user was not found".into()) + })?; + 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 users WHERE user_id = ?1", params![user_id])?; + tx.execute( + r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at) + VALUES (?1, ?2, 'released', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3) + ON CONFLICT(user_id) DO UPDATE SET username = excluded.username, lifecycle_state = 'released', updated_at = excluded.updated_at"#, + params![user_id, username, now_millis()], + )?; + tx.commit()?; + Ok(()) + })?; + 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 +/// erasure delivery. Management metadata and credentials are left intact. +pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::StorageError> { + db::with_db(|conn| { + 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 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( + "UPDATE user_residency SET data_state = 'empty', updated_at = ?2 WHERE user_id = ?1", + params![user_id, now_millis()], + )?; + tx.commit()?; + Ok(()) + })?; + 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())) +} + +/// Complete local erasure is idempotent and is the target for a durable +/// Omega-hosted erasure request after account deletion. +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 users 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())) +} + +pub fn get_residency() -> Vec { + db::with_db(|conn| { + let mut stmt = conn.prepare("SELECT user_id, username, lifecycle_state, data_state FROM user_residency ORDER BY username")?; + let rows = stmt.query_map([], |row| { + let lifecycle: String = row.get(2)?; + Ok(UserResidency { + user_id: row.get(0)?, username: row.get(1)?, + state: if lifecycle == "managed" { LocalUserState::Managed } else { LocalUserState::Released }, + data_present: row.get::<_, String>(3)? == "present", + }) + })?; + rows.collect::, _>>().map_err(Into::into) + }).unwrap_or_default() +} + pub fn clear() { if let Err(e) = db::with_db(|conn| { - conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users;")?; + conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users; DELETE FROM user_residency;")?; Ok(()) }) { eprintln!("Failed to clear users: {}", e); } } -#[allow(dead_code)] -pub async fn load_from_tu(username: &str) -> Result<(), ()> { - let file_content = load_file("", &format!("{}.tu", username)); - let segments = file_content.split("::").collect::>(); - let (uuid_str, _omega_host) = segments[0].split_once('@').unwrap_or((segments[0], "")); - let uuid = uuid_str.parse::().unwrap_or(0); - let b64_private_key = segments[1]; - - let keyring = keyring_from_base64(b64_private_key).unwrap(); - let pub_key_bundle = keyring.public_key_bundle(); - let keyring_b64 = crypto_helper::keyring_to_base64(&keyring); - - let mut bytes = [0u8; 192]; - OsRng.fill_bytes(&mut bytes); - let reset_token = STANDARD.encode(&bytes); - - let user_profile = UserProfile::new( - uuid, - username.to_string(), - Some(username.to_string()), - public_key_bundle_to_base64(&pub_key_bundle), - hex_hash(&keyring_b64), - reset_token, - ); - add_user(user_profile); - Ok(()) -} pub fn save_users() { // No-op: users are auto-saved via SQLite. diff --git a/iota-storage/src/users/user_profile.rs b/iota-storage/src/users/user_profile.rs index 2ecb9cc..5f2abd5 100644 --- a/iota-storage/src/users/user_profile.rs +++ b/iota-storage/src/users/user_profile.rs @@ -1,7 +1,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use base64::{Engine as _, engine::general_purpose}; -use iota_util::file_util::{has_file, load_file, used_dir_space}; +use iota_util::file_util::{read_user_credential_with_legacy, used_dir_space}; use json::{JsonValue, object}; use rand::Rng; use rand::rngs::OsRng; @@ -55,9 +55,11 @@ impl UserProfile { if let Some(d) = &self.display_name { obj["display_name"] = d.clone().into(); } - if has_file("", &format!("{}.tu", self.username.clone())) { - obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into(); - } + // Frontend consumers must never receive private credential material. + obj["has_tu"] = read_user_credential_with_legacy(self.user_id, &self.username) + .map(|credential| credential.is_some()) + .unwrap_or(false) + .into(); obj } diff --git a/iota-storage/src/util/db.rs b/iota-storage/src/util/db.rs index 1a4ca7e..82968e7 100644 --- a/iota-storage/src/util/db.rs +++ b/iota-storage/src/util/db.rs @@ -251,6 +251,21 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> { )?; } + if current_version < 7 { + conn.execute_batch( + r#" + CREATE TABLE IF NOT EXISTS user_residency ( + user_id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + lifecycle_state TEXT NOT NULL CHECK (lifecycle_state IN ('managed', 'released')), + data_state TEXT NOT NULL CHECK (data_state IN ('present', 'empty')), + updated_at INTEGER NOT NULL + ); + PRAGMA user_version = 7; + "#, + )?; + } + Ok(()) } @@ -320,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, 6); + 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")?; @@ -337,8 +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, 6); - for table in ["sync_heads", "sync_events", "client_sync_state"] { + 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], diff --git a/iota-storage/src/util/e2ee_storage.rs b/iota-storage/src/util/e2ee_storage.rs index 9d628d4..6b13018 100644 --- a/iota-storage/src/util/e2ee_storage.rs +++ b/iota-storage/src/util/e2ee_storage.rs @@ -188,6 +188,23 @@ pub fn delete_pending_chat_secret_forward( }) } +/// 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 pending_chat_secret_forwards WHERE recipient_user_id = ?1 OR sender_user_id = ?1", + params![user_id], + )?; + tx.commit()?; + Ok(()) + }) +} + pub fn get_chat_secret(query: ChatSecretQuery) -> Result, StorageError> { if query.user_id.is_empty() || query.chat_id.is_empty() { return Ok(None); diff --git a/iota-util/src/file_util.rs b/iota-util/src/file_util.rs index 1e1cad4..8412b10 100755 --- a/iota-util/src/file_util.rs +++ b/iota-util/src/file_util.rs @@ -33,11 +33,70 @@ fn delete_dir_recursive(directory: &Path) -> bool { } #[allow(dead_code)] -pub fn delete_user_directory(user_id: i64) { +pub fn delete_user_directory(user_id: i64) -> io::Result<()> { let user_dir = Path::new(&get_directory()) .join("users") .join(user_id.to_string()); - let _ = delete_dir_recursive(&user_dir); + if !user_dir.exists() { + return Ok(()); + } + fs::remove_dir_all(user_dir) +} + +pub fn credential_path(user_id: i64) -> PathBuf { + storage_directory().join("credentials").join(format!("{user_id}.tu")) +} + +pub fn read_user_credential(user_id: i64) -> io::Result> { + let path = credential_path(user_id); + match fs::read_to_string(path) { + Ok(value) => Ok(Some(value)), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(error), + } +} + +/// 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> { + if let Some(credential) = read_user_credential(user_id)? { + return Ok(Some(credential)); + } + let legacy = storage_file("", format!("{username}.tu"))?; + let credential = match fs::read_to_string(&legacy) { + Ok(value) => value, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(error), + }; + 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")); + } + write_user_credential(user_id, &parsed.to_canonical_string())?; + fs::remove_file(legacy)?; + Ok(Some(parsed.to_canonical_string())) +} + +pub fn write_user_credential(user_id: i64, credential: &str) -> io::Result<()> { + let path = credential_path(user_id); + let parent = path.parent().expect("credential path has parent"); + fs::create_dir_all(parent)?; + let temporary = parent.join(format!(".{user_id}.tu.tmp")); + fs::write(&temporary, credential)?; + if let Err(error) = fs::rename(&temporary, &path) { + let _ = fs::remove_file(&temporary); + return Err(error); + } + Ok(()) +} + +pub fn remove_user_credential(user_id: i64) -> io::Result<()> { + match fs::remove_file(credential_path(user_id)) { + Ok(()) => Ok(()), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error), + } } pub fn load_file_buf(path: &str, name: &str) -> io::Result> { diff --git a/iota-util/src/lib.rs b/iota-util/src/lib.rs index 2277b2b..5d32aef 100644 --- a/iota-util/src/lib.rs +++ b/iota-util/src/lib.rs @@ -1,3 +1,4 @@ pub mod crypto_helper; pub mod crypto_util; pub mod file_util; +pub mod tu; diff --git a/iota-util/src/tu.rs b/iota-util/src/tu.rs new file mode 100644 index 0000000..e1a8334 --- /dev/null +++ b/iota-util/src/tu.rs @@ -0,0 +1,95 @@ +//! Strict parsing and storage-independent handling of user credentials. +//! +//! A `.tu` file is deliberately identified by the account id embedded in its +//! contents. Its filename is presentation data owned by the CLI, never an +//! account authority. + +use crate::crypto_helper::{keyring_from_base64, keyring_to_base64}; +use mtp::crypto::{Keyring, PublicKeyBundle}; +use std::fmt; + +pub const MAX_PROTOCOL_ID: i64 = (1_i64 << 48) - 1; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TuError { + InvalidFormat, + InvalidUserId, + InvalidKeyring, +} + +impl fmt::Display for TuError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::InvalidFormat => "invalid .tu credential format", + Self::InvalidUserId => "invalid .tu user id", + Self::InvalidKeyring => "invalid .tu keyring", + }) + } +} + +impl std::error::Error for TuError {} + +pub struct TuCredential { + pub user_id: i64, + pub omega_host: String, + pub keyring: Keyring, +} + +impl fmt::Debug for TuCredential { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TuCredential") + .field("user_id", &self.user_id) + .field("omega_host", &self.omega_host) + .field("keyring", &"") + .finish() + } +} + +impl TuCredential { + pub fn parse(input: &str) -> Result { + let (identity, encoded_keyring) = input.trim().split_once("::").ok_or(TuError::InvalidFormat)?; + if encoded_keyring.is_empty() || encoded_keyring.contains("::") { + return Err(TuError::InvalidFormat); + } + let (user_id, omega_host) = identity.split_once('@').ok_or(TuError::InvalidFormat)?; + if omega_host.trim().is_empty() || omega_host.contains('@') { + return Err(TuError::InvalidFormat); + } + let user_id = user_id.parse::().map_err(|_| TuError::InvalidUserId)?; + if !(1..=MAX_PROTOCOL_ID).contains(&user_id) { + 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 }) + } + + pub fn public_key_bundle(&self) -> PublicKeyBundle { + self.keyring.public_key_bundle() + } + + pub fn to_canonical_string(&self) -> String { + format!("{}@{}::{}", self.user_id, self.omega_host, keyring_to_base64(&self.keyring)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto_helper::generate_keyring; + + #[test] + fn round_trip_is_canonical() { + 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()); + } + + #[test] + fn rejects_malformed_credentials() { + 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/Cargo.toml b/iota/Cargo.toml index 3c38d65..b05cfe8 100644 --- a/iota/Cargo.toml +++ b/iota/Cargo.toml @@ -11,6 +11,7 @@ iota-core = { path = "../iota-core" } iota-process-manager = { path = "../iota-process-manager" } iota-paths = { path = "../iota-paths" } iota-terms = { path = "../iota-terms" } +iota-util = { path = "../iota-util" } tokio = { version = "1.50.0", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } serde_json = "1" diff --git a/iota/src/cli_args.rs b/iota/src/cli_args.rs index 48c8ead..b1447a0 100644 --- a/iota/src/cli_args.rs +++ b/iota/src/cli_args.rs @@ -1,6 +1,7 @@ use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum, error::ErrorKind}; use iota_cli::theme::{CliOutputFormat, ThemeName, UiConfig}; use iota_terms::TermsType; +use std::path::PathBuf; #[derive(Debug)] pub struct CliInvocation { @@ -115,15 +116,33 @@ enum UsersAction { user_id: i64, }, Add { - username: String, + username: Option, + #[arg(long, value_name = "PATH")] + tu: Option, }, - Remove { + Release { user_id: i64, #[arg(long)] yes: bool, }, - Import { - username: String, + Data { + #[command(subcommand)] + action: UserDataAction, + }, + CompleteDelete { + user_id: i64, + #[arg(long, value_name = "PATH")] + tu: Option, + #[arg(long)] + yes: bool, + }, +} +#[derive(Subcommand, Debug)] +enum UserDataAction { + Purge { + user_id: i64, + #[arg(long)] + yes: bool, }, } #[derive(Args, Debug)] @@ -264,14 +283,21 @@ pub enum Command { user_id: i64, }, UsersAdd { - username: String, + username: Option, + tu: Option, }, - UsersRemove { + UsersRelease { user_id: i64, confirmed: bool, }, - UsersImport { - username: String, + UsersPurgeData { + user_id: i64, + confirmed: bool, + }, + UsersCompleteDelete { + user_id: i64, + tu: Option, + confirmed: bool, }, OmikronReconnect, IdentityRotate { @@ -367,12 +393,29 @@ impl CliInvocation { Some(CliCommand::Users(users)) => match users.action { UsersAction::List => Command::UsersList, UsersAction::Show { user_id } => Command::UsersShow { user_id }, - UsersAction::Add { username } => Command::UsersAdd { username }, - UsersAction::Remove { user_id, yes } => Command::UsersRemove { + UsersAction::Add { username, tu } => { + if username.is_some() == tu.is_some() { + return Err( + "users add requires exactly one of or --tu ".into(), + ); + } + Command::UsersAdd { username, tu } + } + UsersAction::Release { user_id, yes } => Command::UsersRelease { user_id, confirmed: resolve_confirmed(yes), }, - UsersAction::Import { username } => Command::UsersImport { username }, + UsersAction::Data { + action: UserDataAction::Purge { user_id, yes }, + } => Command::UsersPurgeData { + user_id, + confirmed: resolve_confirmed(yes), + }, + UsersAction::CompleteDelete { user_id, tu, yes } => Command::UsersCompleteDelete { + user_id, + tu, + confirmed: resolve_confirmed(yes), + }, }, Some(CliCommand::Omikron(omikron)) => match omikron.action { OmikronAction::Reconnect => Command::OmikronReconnect, @@ -445,6 +488,7 @@ impl CliInvocation { } } + #[allow(unused)] pub fn help_text() -> String { Cli::command().render_long_help().to_string() } @@ -528,7 +572,7 @@ mod tests { #[test] fn command_schema_drives_help_and_completion_paths() { let paths = CliInvocation::command_paths(); - assert!(paths.contains(&"users remove".to_owned())); + assert!(paths.contains(&"users release".to_owned())); assert!(paths.contains(&"daemon install".to_owned())); let help = CliInvocation::help_text(); assert!(help.contains("users")); @@ -558,12 +602,7 @@ mod tests { #[test] fn parses_unconfirmed_destructive_commands_explicitly() { let invocation = CliInvocation::parse(["daemon".into(), "stop".into()]).unwrap(); - assert_eq!( - invocation.command, - Command::DaemonStop { - confirmed: true - } - ); + assert_eq!(invocation.command, Command::DaemonStop { confirmed: true }); } #[test] @@ -573,7 +612,8 @@ mod tests { assert_eq!( invocation.command, Command::UsersAdd { - username: "alice".into() + username: Some("alice".into()), + tu: None, } ); } @@ -597,26 +637,24 @@ mod tests { } #[test] - fn parses_users_remove_without_confirmation() { - let invocation = - CliInvocation::parse(["users".into(), "remove".into(), "42".into()]).unwrap(); - assert_eq!( - invocation.command, - Command::UsersRemove { - user_id: 42, - confirmed: true, - } - ); + fn rejects_ambiguous_users_remove() { + let error = + CliInvocation::parse(["users".into(), "remove".into(), "42".into()]).unwrap_err(); + assert!(error.contains("remove")); } #[test] - fn parses_users_remove_with_confirmation() { - let invocation = - CliInvocation::parse(["users".into(), "remove".into(), "42".into(), "--yes".into()]) - .unwrap(); + fn parses_users_release_with_confirmation() { + let invocation = CliInvocation::parse([ + "users".into(), + "release".into(), + "42".into(), + "--yes".into(), + ]) + .unwrap(); assert_eq!( invocation.command, - Command::UsersRemove { + Command::UsersRelease { user_id: 42, confirmed: true, } diff --git a/iota/src/main.rs b/iota/src/main.rs index 192f1c8..e05b63f 100644 --- a/iota/src/main.rs +++ b/iota/src/main.rs @@ -403,7 +403,9 @@ fn print_help() { println!(" users list List all users"); println!(" users show Show user details"); println!(" users add Create a new user"); - println!(" users remove Remove a user (requires --yes)"); + println!(" users add --tu Add an existing account credential"); + println!(" users data purge Purge hosted data (requires --yes)"); + println!(" users release Release this Iota (requires --yes)"); println!(" omikron status Show Omikron connection status"); println!(" omikron reconnect Reconnect to Omikron"); println!(" identity rotate Rotate identity keys (requires --yes)"); @@ -451,7 +453,7 @@ fn print_help() { println!(" iota status Show daemon status"); println!(" iota users list --output=json List users in JSON format"); println!(" iota users add alice Create a user named 'alice'"); - println!(" iota users remove 42 --yes Remove user 42"); + println!(" iota users data purge 42 --yes Purge hosted data"); println!(" iota config get --output=yaml Show config in YAML format"); println!(" iota logs --limit 50 Show last 50 log entries"); println!(" iota completions bash Generate bash completions"); @@ -564,12 +566,40 @@ async fn run_command( Command::Tasks => LocalRequest::ListTasks, Command::UsersList => LocalRequest::ListUsers, Command::UsersShow { user_id } => LocalRequest::GetUser { user_id }, - Command::UsersAdd { username } => LocalRequest::CreateUser { username }, - Command::UsersRemove { + 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( + "users add requires exactly one of or --tu ".into(), + )); + } + Command::UsersRelease { user_id, confirmed: true, - } => LocalRequest::RemoveUser { user_id }, - Command::UsersImport { username } => LocalRequest::ImportUser { username }, + } => LocalRequest::ReleaseUser { user_id }, + 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())))?; + if parsed.user_id != user_id { + 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 } + } Command::OmikronReconnect => LocalRequest::ReconnectOmikron, Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity, Command::RegenerateKeys { confirmed: true } => LocalRequest::RotateIotaIdentity, @@ -588,9 +618,11 @@ async fn run_command( Command::Logs { limit } => LocalRequest::GetLogs { limit }, Command::UpdateCheck => LocalRequest::CheckUpdate, Command::CommunityList => LocalRequest::ListCommunities, - Command::UsersRemove { + Command::UsersRelease { confirmed: false, .. } + | Command::UsersPurgeData { confirmed: false, .. } + | Command::UsersCompleteDelete { confirmed: false, .. } | Command::IdentityRotate { confirmed: false } | Command::RegenerateKeys { confirmed: false } | Command::DaemonRestart { confirmed: false } @@ -689,6 +721,9 @@ async fn run_command( 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); } @@ -935,6 +970,9 @@ fn render_table(payload: &ResponsePayload) { ResponsePayload::UserRemoved { user_id } => { println!("Removed user {}", user_id); } + ResponsePayload::UserDataPurged { user_id } => { + println!("Purged hosted data for {}", user_id); + } ResponsePayload::Acknowledged { message } => { println!("{}", message); } diff --git a/omikron-connector/src/omikron_connection.rs b/omikron-connector/src/omikron_connection.rs index 81d6ae6..a555398 100755 --- a/omikron-connector/src/omikron_connection.rs +++ b/omikron-connector/src/omikron_connection.rs @@ -896,12 +896,41 @@ impl OmikronConnection { dispatch!(SettingsSave, handle_settings_save); dispatch!(SettingsLoad, handle_settings_load); dispatch!(SettingsList, handle_settings_list); + dispatch!(EraseHostedUserData, handle_erase_hosted_user_data); } // ------------------------------------------------------------------------- // Message Handlers // ------------------------------------------------------------------------- + /// Omega-authorized account cleanup. The storage operation is idempotent; + /// acknowledgement is therefore safe to retry after a reconnect. + async fn handle_erase_hosted_user_data(self: Arc, cv: &CommunicationValue) { + let Some(user_id) = cv + .get_data(DataType::UserId) + .as_signed_number() + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) + else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + + if iota_storage::users::user_manager::erase_user_locally(user_id).is_err() { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInternal)) + .await; + return; + } + + let acknowledgement = CommunicationValue::new(CommunicationType::EraseHostedUserDataAck) + .with_id(cv.get_id()) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())); + 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) { diff --git a/omikron-connector/src/user_ops.rs b/omikron-connector/src/user_ops.rs index fde043f..241fd46 100644 --- a/omikron-connector/src/user_ops.rs +++ b/omikron-connector/src/user_ops.rs @@ -2,9 +2,12 @@ use base64::{Engine as _, engine::general_purpose::STANDARD}; use iota_logger::{PrintType, log, log_cv, log_t}; use iota_storage::users::user_manager::try_add_user; 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::try_save_file; +use iota_util::file_util::write_user_credential; +use iota_util::tu::TuCredential; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme}; use rand_core::{OsRng, RngCore}; use std::time::Duration; @@ -20,6 +23,133 @@ pub enum CreateUserError { LocalPersistence(String), } +#[derive(Debug)] +pub enum LifecycleUserError { + InvalidCredential(String), + OmegaHostMismatch, + RemoteRejected, + Transport(crate::OmikronError), + LocalPersistence(String), +} + +impl From for LifecycleUserError { + fn from(value: crate::OmikronError) -> Self { Self::Transport(value) } +} + +fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec { + let mut payload = Vec::with_capacity(domain.len() + 24); + payload.extend_from_slice(domain); + payload.extend_from_slice(&user_id.to_be_bytes()); + payload.extend_from_slice(&iota_id.to_be_bytes()); + payload.extend_from_slice(&nonce.to_be_bytes()); + payload +} + +fn configured_iota_id() -> Result { + 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())) +} + +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()))?; + Ok((classical, pq)) +} + +async fn inspect_credential_account( + connection: &dyn OmikronClient, + credential: &TuCredential, +) -> Result<(String, String), LifecycleUserError> { + 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?; + if !response.is_type(CommunicationType::GetUserData) { + return Err(LifecycleUserError::RemoteRejected); + } + 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) + .ok_or(LifecycleUserError::RemoteRejected)?; + if public_key != public_key_bundle_to_base64(&credential.public_key_bundle()) { + return Err(LifecycleUserError::RemoteRejected); + } + Ok((username, public_key)) +} + +async fn credential_proof( + connection: &dyn OmikronClient, + credential: &TuCredential, + begin: CommunicationType, + challenge: CommunicationType, + complete: CommunicationType, + 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?; + if !challenge_response.is_type(challenge) { + return Err(LifecycleUserError::RemoteRejected); + } + 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 complete_request = CommunicationValue::new(complete) + .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) } +} + +/// 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()))?; + 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()); + 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()))?; + 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())); } + inspect_credential_account(connection, &credential).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; }; + 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()); + if remote_iota_id != Some(local_iota_id) { + let _ = iota_storage::users::user_manager::release_user(user.user_id); + } + } +} + fn valid_username(username: &str) -> bool { !username.is_empty() && username.chars().count() <= 15 @@ -136,15 +266,9 @@ pub async fn create_user( } } log!("Created User"); - try_save_file( - "", - &format!("{}.tu", username), - &format!( - "{}@{}::{}", - user_id, - omega_discovery::omega_host(), - keyring_b64 - ), + write_user_credential( + user_id, + &format!("{}@{}::{}", user_id, omega_discovery::omega_host(), keyring_b64), ) .map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?; From c502dfe83a4c22040e705470afb815895bfb8099 Mon Sep 17 00:00:00 2001 From: Rasensprenger Date: Sun, 9 Aug 2026 21:01:55 +0200 Subject: [PATCH 3/3] Update Rust crate aes-gcm to 0.11.0 --- Cargo.lock | 105 ++++++++++++++++++++++++++++++---------- client/Cargo.toml | 2 +- communities/Cargo.toml | 2 +- iota-auth/Cargo.toml | 2 +- iota-cli/Cargo.toml | 2 +- iota-storage/Cargo.toml | 2 +- iota-updater/Cargo.toml | 2 +- other-iota/Cargo.toml | 2 +- web-ui/Cargo.toml | 2 +- 9 files changed, 88 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1003ddf..705b64d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,6 +263,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "aes" version = "0.8.4" @@ -270,19 +280,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] [[package]] -name = "aes-gcm" -version = "0.10.3" +name = "aes" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ - "aead", - "aes", - "cipher", + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead 0.6.1", + "aes 0.9.2", + "cipher 0.5.2", "ctr", "ghash", "subtle", @@ -686,7 +707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -707,9 +728,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", + "aead 0.5.2", "chacha20 0.9.1", - "cipher", + "cipher 0.4.4", "poly1305", "zeroize", ] @@ -734,10 +755,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", - "inout", + "inout 0.1.4", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "clap" version = "4.6.6" @@ -939,6 +971,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1063,11 +1101,11 @@ dependencies = [ [[package]] name = "ctr" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -1701,11 +1739,10 @@ dependencies = [ [[package]] name = "ghash" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "opaque-debug", "polyval", ] @@ -2225,6 +2262,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "instability" version = "0.3.13" @@ -3841,19 +3887,18 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] name = "polyval" -version = "0.6.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", ] [[package]] @@ -5343,6 +5388,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -6156,7 +6211,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" dependencies = [ - "aes", + "aes 0.8.4", "arbitrary", "bzip2", "constant_time_eq", diff --git a/client/Cargo.toml b/client/Cargo.toml index 174b5fa..c99cbc9 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -14,7 +14,7 @@ iota-auth = { path = "../iota-auth" } actix-web = { version = "4", features = ["rustls-0_23"] } actix-web-actors = "4" -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" async-trait = "0.1.89" base64 = "0.22.1" chrono = "0.4.43" diff --git a/communities/Cargo.toml b/communities/Cargo.toml index 89c4f57..9e27fb7 100644 --- a/communities/Cargo.toml +++ b/communities/Cargo.toml @@ -13,7 +13,7 @@ iota-auth = { path = "../iota-auth" } actix-web = { version = "4", features = ["rustls-0_23"] } actix-web-actors = "4" -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" async-trait = "0.1.89" base64 = "0.22.1" chrono = "0.4.43" diff --git a/iota-auth/Cargo.toml b/iota-auth/Cargo.toml index dc43347..15476c9 100644 --- a/iota-auth/Cargo.toml +++ b/iota-auth/Cargo.toml @@ -10,7 +10,7 @@ iota-util = { path = "../iota-util" } iota-storage = { path = "../iota-storage" } iota-state = { path = "../iota-state" } -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" async-trait = "0.1.89" base64 = "0.22.1" chrono = "0.4.43" diff --git a/iota-cli/Cargo.toml b/iota-cli/Cargo.toml index 4c3d508..f06cd70 100644 --- a/iota-cli/Cargo.toml +++ b/iota-cli/Cargo.toml @@ -28,7 +28,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true } actix-web = { version = "4", features = ["rustls-0_23"] } actix-web-actors = "4" -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" async-trait = "0.1.89" base64 = "0.22.1" chrono = "0.4.43" diff --git a/iota-storage/Cargo.toml b/iota-storage/Cargo.toml index caf6417..438afc0 100644 --- a/iota-storage/Cargo.toml +++ b/iota-storage/Cargo.toml @@ -11,7 +11,7 @@ iota-paths = { path = "../iota-paths" } mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" base64 = "0.22.1" hex = "*" hkdf = "0.12.4" diff --git a/iota-updater/Cargo.toml b/iota-updater/Cargo.toml index 2f6bf04..0b4f0e6 100644 --- a/iota-updater/Cargo.toml +++ b/iota-updater/Cargo.toml @@ -18,7 +18,7 @@ sysinfo = "0.39.0" uuid = { version = "*", features = ["v4"] } walkdir = "2.5.0" zip = "6.0.0" -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" base64 = "0.22.1" rand_core = { version = "0.6", features = ["getrandom", "std"] } sha2 = "0.11.0" diff --git a/other-iota/Cargo.toml b/other-iota/Cargo.toml index a6933c3..01653fd 100644 --- a/other-iota/Cargo.toml +++ b/other-iota/Cargo.toml @@ -13,7 +13,7 @@ iota-auth = { path = "../iota-auth" } actix-web = { version = "4", features = ["rustls-0_23"] } actix-web-actors = "4" -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" async-trait = "0.1.89" base64 = "0.22.1" chrono = "0.4.43" diff --git a/web-ui/Cargo.toml b/web-ui/Cargo.toml index 988aac1..9318d94 100644 --- a/web-ui/Cargo.toml +++ b/web-ui/Cargo.toml @@ -13,7 +13,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } actix-web = { version = "4", features = ["rustls-0_23"] } actix-web-actors = "4" -aes-gcm = "0.10.3" +aes-gcm = "0.11.0" async-trait = "0.1.89" base64 = "0.22.1" chrono = "0.4.43"