Compare commits

..
Author SHA1 Message Date
c502dfe83a Update Rust crate aes-gcm to 0.11.0
Some checks failed
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
2026-08-09 21:01:55 +02:00
7dc98ef29b
[Fix] User deletion & migration 2026-08-09 02:51:47 +02:00
326ebf3b37
[Fix] Replies 2026-08-08 02:09:38 +02:00
23 changed files with 1045 additions and 269 deletions

321
Cargo.lock generated
View file

@ -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",
@ -311,9 +311,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",
]
@ -341,9 +341,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",
]
@ -504,9 +504,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",
@ -515,9 +515,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",
@ -532,6 +532,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"
@ -672,9 +678,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",
@ -812,7 +818,7 @@ dependencies = [
"actix-web-actors",
"aes-gcm",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"crossterm",
"dashmap",
@ -1127,6 +1133,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"
@ -1140,9 +1162,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",
@ -1150,26 +1172,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]]
@ -1188,9 +1210,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"
@ -1211,7 +1233,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
@ -1222,6 +1243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d"
dependencies = [
"const-oid 0.10.2",
"pem-rfc7468",
"zeroize",
]
@ -1348,20 +1370,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"
@ -1435,12 +1482,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"
@ -1471,6 +1512,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"
@ -1484,9 +1531,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"
@ -1735,7 +1782,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.4.2",
"http 1.5.0",
"indexmap",
"slab",
"tokio",
@ -1752,7 +1799,7 @@ dependencies = [
"bytes",
"fastrand",
"futures-util",
"http 1.4.2",
"http 1.5.0",
"pin-project-lite",
"tokio",
]
@ -1793,7 +1840,7 @@ dependencies = [
"futures-util",
"h3",
"h3-datagram",
"http 1.4.2",
"http 1.5.0",
"pin-project-lite",
"tokio",
"tracing",
@ -1842,10 +1889,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",
@ -1857,7 +1904,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]]
@ -1927,9 +1974,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",
@ -1942,7 +1989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
"bytes",
"http 1.4.2",
"http 1.5.0",
]
[[package]]
@ -1953,7 +2000,7 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
dependencies = [
"bytes",
"futures-core",
"http 1.4.2",
"http 1.5.0",
"http-body",
"pin-project-lite",
]
@ -1972,9 +2019,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",
@ -1991,7 +2038,7 @@ dependencies = [
"futures-channel",
"futures-core",
"h2 0.4.15",
"http 1.4.2",
"http 1.5.0",
"http-body",
"httparse",
"httpdate",
@ -2008,7 +2055,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",
@ -2023,18 +2070,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",
@ -2226,15 +2273,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]]
@ -2249,6 +2296,7 @@ dependencies = [
"iota-paths",
"iota-process-manager",
"iota-terms",
"iota-util",
"serde_json",
"serde_yaml",
"tokio",
@ -2261,7 +2309,7 @@ version = "0.1.0"
dependencies = [
"aes-gcm",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"crossterm",
"dashmap",
@ -2311,7 +2359,7 @@ dependencies = [
"actix-web-actors",
"aes-gcm",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"crossterm",
"dashmap",
@ -2503,7 +2551,7 @@ version = "0.1.0"
dependencies = [
"aes-gcm",
"arc-swap",
"base64",
"base64 0.22.1",
"hex",
"hkdf 0.12.4",
"iota-logger",
@ -2548,8 +2596,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",
@ -2578,7 +2626,7 @@ dependencies = [
name = "iota-util"
version = "0.1.0"
dependencies = [
"base64",
"base64 0.22.1",
"hex",
"iota-paths",
"mtp",
@ -2592,9 +2640,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"
@ -2743,9 +2791,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",
@ -2799,9 +2847,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",
]
@ -2858,9 +2906,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",
]
@ -3012,7 +3060,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",
@ -3028,7 +3076,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",
@ -3041,9 +3089,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",
@ -3054,7 +3102,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",
@ -3065,11 +3113,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",
@ -3079,7 +3127,7 @@ dependencies = [
"rustls",
"serde",
"sha2 0.11.0",
"thiserror 1.0.69",
"thiserror 2.0.19",
"tokio",
"zeroize",
]
@ -3087,24 +3135,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",
@ -3113,7 +3161,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",
@ -3131,7 +3179,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",
@ -3140,14 +3188,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",
@ -3352,7 +3400,7 @@ name = "omikron-connector"
version = "0.1.0"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"dashmap",
"hex",
"iota-connection",
@ -3460,7 +3508,7 @@ dependencies = [
"actix-web-actors",
"aes-gcm",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"crossterm",
"dashmap",
@ -3505,21 +3553,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",
@ -3527,6 +3575,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"
@ -3566,15 +3623,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",
]
@ -3903,7 +3960,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.5",
"socket2",
"thiserror 2.0.19",
"tokio",
"tracing",
@ -3944,7 +4001,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.5",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
@ -4182,9 +4239,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",
@ -4209,12 +4266,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",
@ -4612,7 +4669,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",
]
@ -4712,16 +4769,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"
@ -4958,7 +5005,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",
@ -5035,9 +5082,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",
@ -5102,20 +5149,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]]
@ -5202,7 +5249,7 @@ dependencies = [
"bitflags 2.13.1",
"bytes",
"futures-util",
"http 1.4.2",
"http 1.5.0",
"http-body",
"pin-project-lite",
"tower",
@ -5269,7 +5316,7 @@ checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
dependencies = [
"bytes",
"data-encoding",
"http 1.4.2",
"http 1.5.0",
"httparse",
"log",
"native-tls",
@ -5454,7 +5501,7 @@ dependencies = [
"bytes",
"futures-util",
"headers",
"http 1.4.2",
"http 1.5.0",
"http-body",
"http-body-util",
"log",
@ -5547,7 +5594,7 @@ name = "web-server"
version = "0.1.0"
dependencies = [
"bytes",
"http 1.4.2",
"http 1.5.0",
"iota-logger",
"iota-util",
"mtp",
@ -5583,7 +5630,7 @@ dependencies = [
"actix-web-actors",
"aes-gcm",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"crossterm",
"dashmap",
@ -5967,7 +6014,7 @@ dependencies = [
"rustls-native-certs",
"rustls-pki-types",
"sha2 0.11.0",
"socket2 0.6.5",
"socket2",
"thiserror 2.0.19",
"time",
"tokio",
@ -5995,7 +6042,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",
@ -6066,18 +6113,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",
@ -6187,9 +6234,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"

View file

@ -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;

View file

@ -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(),

View file

@ -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();

View file

@ -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())
}

View file

@ -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);

View file

@ -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<UserSummary> = user_manager::get_users()
let users: Vec<UserSummary> = 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)

View file

@ -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(),

View file

@ -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};

View file

@ -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("<redacted>")
}
}
// ---------------------------------------------------------------------------
// 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<SecretString>,
},
/// 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<TaskSummary>),
Users(Vec<UserSummary>),
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<String>,
pub created_at: i64,
pub trusted_apps: Vec<String>,
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)]

View file

@ -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<UserResidency> {
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::<Result<Vec<_>, _>>().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::<Vec<&str>>();
let (uuid_str, _omega_host) = segments[0].split_once('@').unwrap_or((segments[0], ""));
let uuid = uuid_str.parse::<i64>().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.

View file

@ -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
}

View file

@ -644,6 +644,62 @@ pub fn get_messages(
}
}
pub fn get_message(
storage_owner: i64,
message_time: i64,
external_user: Option<i64>,
) -> Result<Option<StoredMessage>, 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<StoredMessage> = rows.collect::<Result<_, _>>()?;
if messages.is_empty() {
return Ok(None);
}
if external_user.is_none()
&& messages
.iter()
.map(|message| message.external_user)
.collect::<std::collections::HashSet<_>>()
.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<StoredMessage> {
if ids.is_empty() {
return Vec::new();

View file

@ -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],

View file

@ -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<Option<StoredChatSecret>, StorageError> {
if query.user_id.is_empty() || query.chat_id.is_empty() {
return Ok(None);

View file

@ -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<Option<String>> {
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
/// `<username>.tu` is migrated atomically the first time it is encountered.
pub fn read_user_credential_with_legacy(user_id: i64, username: &str) -> io::Result<Option<String>> {
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<BufReader<File>> {

View file

@ -1,3 +1,4 @@
pub mod crypto_helper;
pub mod crypto_util;
pub mod file_util;
pub mod tu;

95
iota-util/src/tu.rs Normal file
View file

@ -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", &"<redacted>")
.finish()
}
}
impl TuCredential {
pub fn parse(input: &str) -> Result<Self, TuError> {
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::<i64>().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}");
}
}
}

View file

@ -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"

View file

@ -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<String>,
#[arg(long, value_name = "PATH")]
tu: Option<PathBuf>,
},
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<PathBuf>,
#[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<String>,
tu: Option<PathBuf>,
},
UsersRemove {
UsersRelease {
user_id: i64,
confirmed: bool,
},
UsersImport {
username: String,
UsersPurgeData {
user_id: i64,
confirmed: bool,
},
UsersCompleteDelete {
user_id: i64,
tu: Option<PathBuf>,
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 <username> or --tu <PATH>".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,
}

View file

@ -403,7 +403,9 @@ fn print_help() {
println!(" users list List all users");
println!(" users show <ID> Show user details");
println!(" users add <NAME> Create a new user");
println!(" users remove <ID> Remove a user (requires --yes)");
println!(" users add --tu <PATH> Add an existing account credential");
println!(" users data purge <ID> Purge hosted data (requires --yes)");
println!(" users release <ID> 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 <username> or --tu <PATH>".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);
}

View file

@ -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);
@ -895,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<Self>, 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<Self>, cv: &CommunicationValue) {
let sender_id = cv.get_sender().to_string();
let recipients = match chat_secret_recipients(cv) {
@ -1447,6 +1477,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 +1749,12 @@ impl OmikronConnection {
.await;
}
async fn handle_message_get(self: Arc<Self>, cv: &CommunicationValue) {
let _ = self
.send_message(&message_handlers::handle_message_get(cv))
.await;
}
async fn handle_get_chats(self: Arc<Self>, cv: &CommunicationValue) {
let _ = self
.send_message(&message_handlers::handle_get_chats(cv))

View file

@ -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<crate::OmikronError> 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<u8> {
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<i64, LifecycleUserError> {
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<u8>, Vec<u8>), 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<UserProfile, LifecycleUserError> {
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()))?;