[WIP] 0.3.0 mtp update

This commit is contained in:
Alex Emmet 2026-08-18 22:39:02 +02:00
commit e1dd86ec02
No known key found for this signature in database
42 changed files with 2422 additions and 1429 deletions

342
Cargo.lock generated
View file

@ -44,9 +44,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.13.2" version = "3.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53200bd1513e569e6e644181c922cec072c121a27db5b38d45e88e630c369366" checksum = "11004b0e9b44b4eb3d15e0c3132b96fb178c7e50a74758b2f17bb9cc9a7fb4f6"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
@ -175,9 +175,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web" name = "actix-web"
version = "4.14.0" version = "4.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" checksum = "58356675d8c86d2e720480645a0316808471a62d0073f6a3b98810a5e0ca0e73"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@ -410,6 +410,18 @@ dependencies = [
"rustversion", "rustversion",
] ]
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures 0.2.17",
"password-hash",
]
[[package]] [[package]]
name = "asn1-rs" name = "asn1-rs"
version = "0.7.2" version = "0.7.2"
@ -422,7 +434,7 @@ dependencies = [
"nom", "nom",
"num-traits", "num-traits",
"rusticata-macros", "rusticata-macros",
"thiserror 2.0.19", "thiserror 2.0.20",
"time", "time",
] ]
@ -451,9 +463,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.91" version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -559,6 +571,15 @@ version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.7",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -657,9 +678,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.4.1" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9066c49992464636f92905fa096ec58baaa4d57ec19a5c096c68d3e25ef3d136" checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@ -1493,9 +1514,9 @@ dependencies = [
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.10" version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]] [[package]]
name = "finl_unicode" name = "finl_unicode"
@ -1564,9 +1585,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1579,9 +1600,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -1589,15 +1610,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -1606,38 +1627,38 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 3.0.3",
] ]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1736,9 +1757,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.15" version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@ -1957,9 +1978,9 @@ dependencies = [
[[package]] [[package]]
name = "http-body-util" name = "http-body-util"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@ -2000,7 +2021,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"h2 0.4.15", "h2 0.4.16",
"http 1.5.0", "http 1.5.0",
"http-body", "http-body",
"httparse", "httparse",
@ -2078,9 +2099,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"potential_utf", "potential_utf",
@ -2092,9 +2113,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_locale_core" name = "icu_locale_core"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"litemap", "litemap",
@ -2105,9 +2126,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer" name = "icu_normalizer"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [ dependencies = [
"icu_collections", "icu_collections",
"icu_normalizer_data", "icu_normalizer_data",
@ -2119,16 +2140,17 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer_data" name = "icu_normalizer_data"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [ dependencies = [
"displaydoc",
"icu_collections", "icu_collections",
"icu_locale_core", "icu_locale_core",
"icu_properties_data", "icu_properties_data",
@ -2139,15 +2161,15 @@ dependencies = [
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_locale_core", "icu_locale_core",
@ -2371,6 +2393,7 @@ dependencies = [
"iota-storage", "iota-storage",
"iota-util", "iota-util",
"mtp", "mtp",
"tokio",
] ]
[[package]] [[package]]
@ -2526,7 +2549,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"sha2 0.11.0", "sha2 0.11.0",
"sysinfo", "sysinfo",
"thiserror 2.0.19", "thiserror 2.0.20",
"tokio", "tokio",
"uuid", "uuid",
"walkdir", "walkdir",
@ -2659,7 +2682,7 @@ dependencies = [
"jni-sys", "jni-sys",
"log", "log",
"simd_cesu8", "simd_cesu8",
"thiserror 2.0.19", "thiserror 2.0.20",
"walkdir", "walkdir",
"windows-link", "windows-link",
] ]
@ -2708,9 +2731,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.103" version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@ -2731,7 +2754,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
dependencies = [ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
"portable-atomic", "portable-atomic",
"thiserror 2.0.19", "thiserror 2.0.20",
] ]
[[package]] [[package]]
@ -2791,9 +2814,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.38.1" version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
dependencies = [ dependencies = [
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
@ -2816,9 +2839,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]] [[package]]
name = "litrs" name = "litrs"
@ -3013,8 +3036,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp" name = "mtp"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"mtp-client", "mtp-client",
"mtp-codec", "mtp-codec",
@ -3029,8 +3052,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp-client" name = "mtp-client"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
@ -3042,8 +3065,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp-codec" name = "mtp-codec"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.23.1",
"byteorder", "byteorder",
@ -3051,25 +3074,27 @@ dependencies = [
"mtp-crypto", "mtp-crypto",
"mtp-type-map", "mtp-type-map",
"rand 0.10.2", "rand 0.10.2",
"thiserror 2.0.20",
] ]
[[package]] [[package]]
name = "mtp-common" name = "mtp-common"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"quinn", "quinn",
"rustls", "rustls",
"thiserror 2.0.19", "thiserror 2.0.20",
"wtransport", "wtransport",
] ]
[[package]] [[package]]
name = "mtp-crypto" name = "mtp-crypto"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"base64 0.23.1", "argon2",
"base64 0.22.1",
"chacha20poly1305", "chacha20poly1305",
"ed25519-dalek 3.0.0", "ed25519-dalek 3.0.0",
"getrandom 0.4.3", "getrandom 0.4.3",
@ -3077,36 +3102,37 @@ dependencies = [
"ml-dsa", "ml-dsa",
"mlkem-tls", "mlkem-tls",
"rand 0.10.2", "rand 0.10.2",
"rand_core 0.10.1", "rand_core 0.6.4",
"rustls", "rustls",
"serde", "serde",
"sha2 0.11.0", "sha2 0.11.0",
"thiserror 2.0.19", "thiserror 1.0.69",
"tokio", "tokio",
"zeroize", "zeroize",
] ]
[[package]] [[package]]
name = "mtp-files" name = "mtp-files"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"mtp-crypto", "mtp-crypto",
"rand 0.10.2", "rand 0.10.2",
"thiserror 2.0.19", "thiserror 2.0.20",
"zeroize", "zeroize",
] ]
[[package]] [[package]]
name = "mtp-host" name = "mtp-host"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
"mtp-crypto", "mtp-crypto",
"mtp-transport", "mtp-transport",
"rand 0.10.2", "rand 0.10.2",
"thiserror 2.0.20",
"tokio", "tokio",
"tracing", "tracing",
"wtransport", "wtransport",
@ -3114,13 +3140,14 @@ dependencies = [
[[package]] [[package]]
name = "mtp-transport" name = "mtp-transport"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
"mtp-crypto", "mtp-crypto",
"rand 0.10.2",
"rcgen", "rcgen",
"rustls", "rustls",
"rustls-native-certs", "rustls-native-certs",
@ -3128,12 +3155,13 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"wtransport", "wtransport",
"zeroize",
] ]
[[package]] [[package]]
name = "mtp-type-map" name = "mtp-type-map"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"serde", "serde",
"serde_yaml", "serde_yaml",
@ -3141,8 +3169,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp-webserver" name = "mtp-webserver"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/Methanium/mtp.git#b067614a684eb1856bc5db7b3fd82148c036ce6b" source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -3159,9 +3187,8 @@ dependencies = [
"mtp-host", "mtp-host",
"mtp-transport", "mtp-transport",
"quinn", "quinn",
"rand 0.10.2",
"rustls", "rustls",
"thiserror 2.0.19", "thiserror 2.0.20",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-stream", "tokio-stream",
@ -3252,9 +3279,9 @@ dependencies = [
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [ dependencies = [
"num-traits", "num-traits",
] ]
@ -3561,6 +3588,17 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "pbkdf2" name = "pbkdf2"
version = "0.12.2" version = "0.12.2"
@ -3598,9 +3636,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.8.8" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf"
dependencies = [ dependencies = [
"memchr", "memchr",
"ucd-trie", "ucd-trie",
@ -3608,9 +3646,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_derive" name = "pest_derive"
version = "2.8.8" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d"
dependencies = [ dependencies = [
"pest", "pest",
"pest_generator", "pest_generator",
@ -3618,9 +3656,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_generator" name = "pest_generator"
version = "2.8.8" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a"
dependencies = [ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
@ -3631,9 +3669,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_meta" name = "pest_meta"
version = "2.8.8" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496"
dependencies = [ dependencies = [
"pest", "pest",
] ]
@ -3738,9 +3776,9 @@ dependencies = [
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.33" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]] [[package]]
name = "pnet" name = "pnet"
@ -3858,15 +3896,15 @@ dependencies = [
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [ dependencies = [
"zerovec", "zerovec",
] ]
@ -3916,7 +3954,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2",
"thiserror 2.0.19", "thiserror 2.0.20",
"tokio", "tokio",
"tracing", "tracing",
"web-time", "web-time",
@ -3924,9 +3962,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.16" version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"bytes", "bytes",
@ -3941,7 +3979,7 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
"rustls-platform-verifier", "rustls-platform-verifier",
"slab", "slab",
"thiserror 2.0.19", "thiserror 2.0.20",
"tinyvec", "tinyvec",
"tracing", "tracing",
"web-time", "web-time",
@ -4087,7 +4125,7 @@ dependencies = [
"palette", "palette",
"serde", "serde",
"strum", "strum",
"thiserror 2.0.19", "thiserror 2.0.20",
"unicode-segmentation", "unicode-segmentation",
"unicode-truncate", "unicode-truncate",
"unicode-width", "unicode-width",
@ -4158,9 +4196,9 @@ dependencies = [
[[package]] [[package]]
name = "rcgen" name = "rcgen"
version = "0.14.8" version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"pem", "pem",
@ -4225,7 +4263,7 @@ dependencies = [
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"h2 0.4.15", "h2 0.4.16",
"http 1.5.0", "http 1.5.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
@ -4274,14 +4312,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
dependencies = [ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
"thiserror 2.0.19", "thiserror 2.0.20",
] ]
[[package]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.40.1" version = "0.40.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.1",
"fallible-iterator", "fallible-iterator",
@ -4405,9 +4443,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.13" version = "0.103.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"ring", "ring",
@ -5006,11 +5044,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.19" version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [ dependencies = [
"thiserror-impl 2.0.19", "thiserror-impl 2.0.20",
] ]
[[package]] [[package]]
@ -5026,9 +5064,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.19" version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5069,9 +5107,9 @@ dependencies = [
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"zerovec", "zerovec",
@ -5277,7 +5315,7 @@ dependencies = [
"native-tls", "native-tls",
"rand 0.10.2", "rand 0.10.2",
"sha1 0.11.0", "sha1 0.11.0",
"thiserror 2.0.19", "thiserror 2.0.20",
] ]
[[package]] [[package]]
@ -5387,9 +5425,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.24.0" version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
dependencies = [ dependencies = [
"atomic", "atomic",
"getrandom 0.4.3", "getrandom 0.4.3",
@ -5481,9 +5519,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.126" version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -5494,9 +5532,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.76" version = "0.4.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -5504,9 +5542,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.126" version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -5514,9 +5552,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.126" version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@ -5527,9 +5565,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.126" version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -5549,9 +5587,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.103" version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -5941,15 +5979,15 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]] [[package]]
name = "wtransport" name = "wtransport"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4aacf790813ee1956751491800537f4e04af7557b7b370501ccbfbc85963e4" checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036"
dependencies = [ dependencies = [
"bytes", "bytes",
"pem", "pem",
@ -5960,7 +5998,7 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
"sha2 0.11.0", "sha2 0.11.0",
"socket2", "socket2",
"thiserror 2.0.19", "thiserror 2.0.20",
"time", "time",
"tokio", "tokio",
"tracing", "tracing",
@ -5971,13 +6009,13 @@ dependencies = [
[[package]] [[package]]
name = "wtransport-proto" name = "wtransport-proto"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" checksum = "aad9059572c7dbd6901ccef37f3b7321678cd708dcf58a64b1921dbeab7bfede"
dependencies = [ dependencies = [
"httlib-huffman", "httlib-huffman",
"octets", "octets",
"thiserror 2.0.19", "thiserror 2.0.20",
"url", "url",
] ]
@ -6019,7 +6057,7 @@ dependencies = [
"oid-registry", "oid-registry",
"ring", "ring",
"rusticata-macros", "rusticata-macros",
"thiserror 2.0.19", "thiserror 2.0.20",
"time", "time",
] ]
@ -6119,9 +6157,9 @@ dependencies = [
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.4" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"yoke", "yoke",
@ -6130,9 +6168,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.11.6" version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8"
dependencies = [ dependencies = [
"yoke", "yoke",
"zerofrom", "zerofrom",
@ -6141,13 +6179,13 @@ dependencies = [
[[package]] [[package]]
name = "zerovec-derive" name = "zerovec-derive"
version = "0.11.3" version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 3.0.3",
] ]
[[package]] [[package]]

View file

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["client"] } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["client", "crypto"] }
iota-connection = { path = "../iota-connection" } iota-connection = { path = "../iota-connection" }
iota-logger = { path = "../iota-logger" } iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" } iota-util = { path = "../iota-util" }

View file

@ -1,14 +1,14 @@
use dashmap::DashMap; use dashmap::DashMap;
use iota_connection::message_common::*; use iota_connection::message_common::*;
use iota_connection::message_handlers; use iota_connection::message_handlers;
use iota_connection::relay::message_security_class;
use iota_logger::{log_cv_in, log_cv_out, log_t}; use iota_logger::{log_cv_in, log_cv_out, log_t};
use iota_storage::util::chat_files::{self, MessageState, change_message_state};
use iota_storage::util::config_util::CONFIG; use iota_storage::util::config_util::CONFIG;
use iota_storage::util::e2ee_storage::{self, StoredChatSecret};
use iota_util::crypto_helper::keyring_from_base64; use iota_util::crypto_helper::keyring_from_base64;
use iota_util::crypto_util::{self}; use iota_util::crypto_util::{self};
use mtp::client::{Receiver, Sender}; use mtp::client::{Receiver, Sender};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::crypto::Keyring;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -30,6 +30,7 @@ pub struct ClientConnection {
pub waiting_tasks: pub waiting_tasks:
DashMap<u32, Box<dyn Fn(Arc<ClientConnection>, CommunicationValue) -> bool + Send + Sync>>, DashMap<u32, Box<dyn Fn(Arc<ClientConnection>, CommunicationValue) -> bool + Send + Sync>>,
shutdown: Arc<RwLock<bool>>, shutdown: Arc<RwLock<bool>>,
keyring: Arc<RwLock<Option<Arc<Keyring>>>>,
} }
impl ClientConnection { impl ClientConnection {
@ -53,9 +54,31 @@ impl ClientConnection {
shutdown_tx, shutdown_tx,
waiting_tasks, waiting_tasks,
shutdown, shutdown,
keyring: Arc::new(RwLock::new(None)),
} }
} }
pub async fn set_keyring(&self, keyring: Arc<Keyring>) {
*self.keyring.write().await = Some(keyring);
}
async fn local_keyring(&self) -> Result<Arc<Keyring>, String> {
if let Some(keyring) = self.keyring.read().await.as_ref().cloned() {
return Ok(keyring);
}
let keyring_data = CONFIG
.load()
.keyring
.clone()
.ok_or_else(|| "Iota keyring is not configured".to_string())?;
let keyring = keyring_from_base64(&keyring_data)
.ok_or_else(|| "Iota keyring is invalid".to_string())?;
let keyring = Arc::new(keyring);
*self.keyring.write().await = Some(keyring.clone());
Ok(keyring)
}
pub fn start(self: Arc<Self>) { pub fn start(self: Arc<Self>) {
let self_clone = self.clone(); let self_clone = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -96,6 +119,27 @@ impl ClientConnection {
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) { pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
log_cv_in!(&cv); log_cv_in!(&cv);
if cv.is_type(CommunicationType::Relay) {
log_t!(
"relay_from_client_rejected",
"legacy client path has no Relay router".to_string()
);
let _ = self
.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
.await;
return;
}
if matches!(
message_security_class(&cv),
iota_connection::relay::MessageSecurityClass::RelayOnly
) {
let _ = self
.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
.await;
return;
}
let _msg_id = cv.get_id(); let _msg_id = cv.get_id();
if cv.is_type(CommunicationType::Challenge) { if cv.is_type(CommunicationType::Challenge) {
@ -103,82 +147,12 @@ impl ClientConnection {
return; return;
} }
if cv.is_type(CommunicationType::SetChatSecret) {
let sender_id = cv.get_sender().to_string();
let recipients = match chat_secret_recipients(&cv) {
Some(recipients) => recipients,
None => {
self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
.await;
return;
}
};
let now = now_millis_i64();
let chat_id = data_string(&cv, DataType::ChatId);
let secret_id = data_string(&cv, DataType::SecretId);
let version = data_i64(&cv, DataType::VersionNumber);
let wrapping_scheme = data_string(&cv, DataType::WrappingScheme);
let created_at = data_i64(&cv, DataType::CreatedAt).unwrap_or(now);
let Some((((chat_id, secret_id), version), wrapping_scheme)) =
chat_id.zip(secret_id).zip(version).zip(wrapping_scheme)
else {
self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
.await;
return;
};
for recipient in recipients.iter().filter(|item| item.user_id == sender_id) {
if e2ee_storage::put_chat_secret(StoredChatSecret {
user_id: recipient.user_id.clone(),
chat_id: chat_id.clone(),
secret_id: secret_id.clone(),
version,
encrypted_secret: recipient.encrypted_secret.clone(),
kem_ciphertext: recipient.kem_ciphertext.clone(),
wrapping_scheme: wrapping_scheme.clone(),
created_at,
updated_at: now,
})
.is_err()
{
self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
.await;
return;
}
}
for recipient in recipients.iter().filter(|item| item.user_id != sender_id) {
self.send_message(&set_chat_secret_cv_for_recipient(&cv, recipient))
.await;
}
self.send_message(&error_response(&cv, CommunicationType::Success))
.await;
return;
}
if cv.is_type(CommunicationType::GetChatSecret) { if cv.is_type(CommunicationType::GetChatSecret) {
self.send_message(&message_handlers::handle_get_chat_secret(&cv)) self.send_message(&message_handlers::handle_get_chat_secret(&cv))
.await; .await;
return; return;
} }
if cv.is_type(CommunicationType::ChatSecretForward) {
let sender_id = cv.get_sender().to_string();
let recipient_id = data_string(&cv, DataType::RecipientUserId).unwrap_or_default();
if data_string(&cv, DataType::SenderUserId).as_deref() != Some(sender_id.as_str())
|| recipient_id.is_empty()
{
self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
.await;
return;
}
self.send_message(&cv.with_receiver(recipient_id.parse::<u64>().unwrap_or(0)))
.await;
return;
}
if cv.is_type(CommunicationType::SaveAppData) { if cv.is_type(CommunicationType::SaveAppData) {
let sender_id = cv.get_sender(); let sender_id = cv.get_sender();
let _app_data = cv let _app_data = cv
@ -270,137 +244,6 @@ impl ClientConnection {
return; return;
} }
if cv.is_type(CommunicationType::MessageOtherIota) {
let sender_id = &cv.get_sender();
let receiver_id = &cv.get_receiver();
// parse send_time safely (number or string), fallback to now
let send_time_val = cv.get_data(DataType::SendTime);
let now_i64 = now_millis_i64();
let timestamp = if let Some(n) = send_time_val.as_number() {
n as i64
} else if let Some(s) = send_time_val.as_str() {
s.parse::<i64>().unwrap_or(now_i64)
} else {
now_i64
};
// content may be missing or non-string; default to empty string
let content = cv
.get_data(DataType::Content)
.as_str()
.unwrap_or("")
.to_string();
let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64;
let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64);
chat_files::add_message(
timestamp as u128,
false,
*receiver_id as i64,
*sender_id as i64,
&content,
height,
reply_to,
);
// Build user_forward using the parsed numeric timestamp and safe content string
let user_forward = CommunicationValue::new(CommunicationType::MessageLive)
.with_id(cv.get_id())
.with_receiver(*receiver_id)
.add_typed_default(
DataType::SenderId,
DataValue::SignedNumber(*sender_id as i128),
)
.add_typed_default(DataType::Message, {
let mut msg_fields = vec![
(DataType::Content, DataValue::Str(content.clone())),
(
DataType::SendTime,
DataValue::SignedNumber(timestamp as i128),
),
(DataType::Height, DataValue::SignedNumber(height as i128)),
];
if let Some(rt) = reply_to {
msg_fields.push((
DataType::ReplyId,
DataValue::UnsignedNumber(rt as u64 as u128),
));
}
typed_container(msg_fields)
});
let user_resp = self
.clone()
.await_response(&user_forward, Some(Duration::from_secs(10)))
.await;
if let Ok(user_resp) = user_resp {
let ms_raw = user_resp
.get_data(DataType::MessageState)
.as_string()
.unwrap_or_else(|| "".to_string());
let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received);
let _ = change_message_state(
timestamp,
*receiver_id as i64,
*sender_id as i64,
ms.clone(),
);
self.send_message(
&CommunicationValue::new(CommunicationType::MessageState)
.with_id(cv.get_id())
.with_receiver(*sender_id)
.with_sender(*receiver_id)
.add_typed_default(
DataType::SendTime,
DataValue::SignedNumber(timestamp as i128),
)
.add_typed_default(
DataType::ChatPartnerId,
DataValue::SignedNumber(*sender_id as i128),
)
.add_typed_default(
DataType::MessageState,
DataValue::Str(ms.as_str().to_string()),
),
)
.await;
} else {
// Delivery timed out/failed — update stored state and notify sender with numeric timestamp
let _ = chat_files::change_message_state(
timestamp,
*receiver_id as i64,
*sender_id as i64,
MessageState::Sent,
);
self.send_message(
&CommunicationValue::new(CommunicationType::MessageState)
.with_id(cv.get_id())
.with_receiver(*sender_id)
.with_sender(*receiver_id)
.add_typed_default(
DataType::SendTime,
DataValue::SignedNumber(timestamp as i128),
)
.add_typed_default(
DataType::ChatPartnerId,
DataValue::SignedNumber(*receiver_id as i128),
)
.add_typed_default(
DataType::MessageState,
DataValue::Str(MessageState::Sent.as_str().to_string()),
),
)
.await;
}
return;
}
if cv.is_type(CommunicationType::MessagesGet) { if cv.is_type(CommunicationType::MessagesGet) {
self.send_message(&message_handlers::handle_messages_get(&cv)) self.send_message(&message_handlers::handle_messages_get(&cv))
.await; .await;
@ -503,9 +346,7 @@ impl ClientConnection {
} }
async fn handle_challenge(&self, cv: &CommunicationValue) { async fn handle_challenge(&self, cv: &CommunicationValue) {
let kr_str = CONFIG.load().keyring.clone().unwrap(); let Ok(keyring) = self.local_keyring().await else {
let Some(keyring) = keyring_from_base64(&kr_str) else {
return; return;
}; };

View file

@ -2,6 +2,7 @@ use crate::auth::auth_user::AuthUser;
use crate::communities::community::Community; use crate::communities::community::Community;
use crate::communities::interactables::interactable::Interactable; use crate::communities::interactables::interactable::Interactable;
use crate::users::user_manager::get_user; use crate::users::user_manager::get_user;
use iota_util::mtp_compat::CommunicationValueCompat;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
use base64::{Engine as _, engine::general_purpose::STANDARD}; use base64::{Engine as _, engine::general_purpose::STANDARD};
use futures::SinkExt; use futures::SinkExt;

View file

@ -8,6 +8,7 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use json::{JsonValue, array, object}; use json::{JsonValue, array, object};
use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;

View file

@ -1,6 +1,7 @@
use crate::communities::{community::Community, interactables::interactable::Interactable}; use crate::communities::{community::Community, interactables::interactable::Interactable};
use async_trait::async_trait; use async_trait::async_trait;
use json::JsonValue; use json::JsonValue;
use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt};
use std::sync::Arc; use std::sync::Arc;
use std::{any::Any, sync::RwLock}; use std::{any::Any, sync::RwLock};
use uuid::Uuid; use uuid::Uuid;

View file

@ -23,7 +23,7 @@ iota-process-manager = { path = "../iota-process-manager" }
iota-paths = { path = "../iota-paths" } iota-paths = { path = "../iota-paths" }
omikron-connector = { path = "../omikron-connector", optional = true } omikron-connector = { path = "../omikron-connector", optional = true }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true }
actix-web = { version = "4", features = ["rustls-0_23"] } actix-web = { version = "4", features = ["rustls-0_23"] }

View file

@ -189,9 +189,7 @@ impl Screen for ConfirmDialog {
let button_widths: Vec<u16> = self let button_widths: Vec<u16> = self
.buttons .buttons
.iter() .iter()
.map(|b| { .map(|b| crate::controls::button::button_minimum_width(&b.label))
crate::controls::button::button_minimum_width(&b.label)
})
.collect(); .collect();
let total_width: u16 = button_widths.iter().sum(); let total_width: u16 = button_widths.iter().sum();
@ -200,9 +198,7 @@ impl Screen for ConfirmDialog {
let start_x = buttons_area.x + available.saturating_sub(total_width + spacing) / 2; let start_x = buttons_area.x + available.saturating_sub(total_width + spacing) / 2;
let mut x = start_x; let mut x = start_x;
for (i, (button_config, &width)) in for (i, (button_config, &width)) in self.buttons.iter().zip(&button_widths).enumerate() {
self.buttons.iter().zip(&button_widths).enumerate()
{
let button_area = Rect { let button_area = Rect {
x, x,
y: buttons_area.y, y: buttons_area.y,

View file

@ -11,12 +11,17 @@ use ratatui::{
widgets::Paragraph, widgets::Paragraph,
}; };
fn connection_badge(state: &IpcConnectionState, theme: &ResolvedTheme) -> (&'static str, ratatui::style::Style) { fn connection_badge(
state: &IpcConnectionState,
theme: &ResolvedTheme,
) -> (&'static str, ratatui::style::Style) {
match state { match state {
IpcConnectionState::Connected => ("OK", theme.status.success), IpcConnectionState::Connected => ("OK", theme.status.success),
IpcConnectionState::Connecting => ("..", theme.status.warning), IpcConnectionState::Connecting => ("..", theme.status.warning),
IpcConnectionState::Reconnecting { .. } => ("WARN", theme.status.warning), IpcConnectionState::Reconnecting { .. } => ("WARN", theme.status.warning),
IpcConnectionState::Failed { .. } | IpcConnectionState::Incompatible { .. } => ("FAIL", theme.status.error), IpcConnectionState::Failed { .. } | IpcConnectionState::Incompatible { .. } => {
("FAIL", theme.status.error)
}
IpcConnectionState::Disconnected => ("WARN", theme.status.warning), IpcConnectionState::Disconnected => ("WARN", theme.status.warning),
} }
} }
@ -52,7 +57,8 @@ pub fn render_header(
format!(" v{}", daemon.version) format!(" v{}", daemon.version)
}; };
let rows = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area); let rows =
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area);
let cells = Layout::horizontal([ let cells = Layout::horizontal([
Constraint::Min(28), Constraint::Min(28),
Constraint::Length(12), Constraint::Length(12),
@ -94,10 +100,34 @@ pub fn render_header(
hits.register(brand_area, AppAction::OpenMain); hits.register(brand_area, AppAction::OpenMain);
for (index, (top, _bottom, label, intent, action)) in [ for (index, (top, _bottom, label, intent, action)) in [
(cells[1], cells2[1], "Overview", ButtonIntent::Primary, AppAction::OpenOverview), (
(cells[2], cells2[2], "Users", ButtonIntent::Neutral, AppAction::OpenUsers), cells[1],
(cells[3], cells2[3], "Settings", ButtonIntent::Neutral, AppAction::OpenSettings), cells2[1],
(cells[4], cells2[4], "Quit", ButtonIntent::Destructive, AppAction::Quit), "Overview",
ButtonIntent::Primary,
AppAction::OpenOverview,
),
(
cells[2],
cells2[2],
"Users",
ButtonIntent::Neutral,
AppAction::OpenUsers,
),
(
cells[3],
cells2[3],
"Settings",
ButtonIntent::Neutral,
AppAction::OpenSettings,
),
(
cells[4],
cells2[4],
"Quit",
ButtonIntent::Destructive,
AppAction::Quit,
),
] ]
.into_iter() .into_iter()
.enumerate() .enumerate()

View file

@ -186,8 +186,7 @@ impl Screen for SettingsScreen {
let inner = header_block.inner(area); let inner = header_block.inner(area);
frame.render_widget(header_block, area); frame.render_widget(header_block, area);
let sections = Layout::vertical([Constraint::Length(2), Constraint::Min(1)]) let sections = Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).split(inner);
.split(inner);
frame.render_widget( frame.render_widget(
Paragraph::new(format!( Paragraph::new(format!(

View file

@ -122,10 +122,7 @@ impl UsersScreen {
if self.loading { if self.loading {
const SPINNERS: &[u8] = b"|/-\\"; const SPINNERS: &[u8] = b"|/-\\";
let ch = SPINNERS[self.tick.fetch_add(1, Ordering::Relaxed) as usize % SPINNERS.len()]; let ch = SPINNERS[self.tick.fetch_add(1, Ordering::Relaxed) as usize % SPINNERS.len()];
f.render_widget( f.render_widget(Paragraph::new(format!("{ch} Loading users…")), inner);
Paragraph::new(format!("{ch} Loading users…")),
inner,
);
return; return;
} }
if visible_indices.is_empty() { if visible_indices.is_empty() {
@ -149,9 +146,20 @@ impl UsersScreen {
let user = &self.users[*user_index]; let user = &self.users[*user_index];
( (
*user_index, *user_index,
format!("{:>6} {} {}{}", user.user_id, user.username, format!(
match user.state { iota_ipc::LocalUserState::Managed => "managed", iota_ipc::LocalUserState::Released => "released" }, "{:>6} {} {}{}",
if user.data_present { "" } else { ", data purged" }), 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(); .collect();
@ -533,11 +541,14 @@ impl Screen for UsersScreen {
match result { match result {
Ok(()) => { Ok(()) => {
self.pending_dialog = None; self.pending_dialog = None;
if let Some(user) = self.users.iter_mut().find(|user| user.user_id == user_id) { if let Some(user) =
self.users.iter_mut().find(|user| user.user_id == user_id)
{
user.state = iota_ipc::LocalUserState::Released; user.state = iota_ipc::LocalUserState::Released;
user.credential_present = false; user.credential_present = false;
} }
self.message = Some(format!("Released user {user_id}; hosted data retained.")); self.message =
Some(format!("Released user {user_id}; hosted data retained."));
} }
Err(error) => { Err(error) => {
self.dialog = self.pending_dialog.take(); self.dialog = self.pending_dialog.take();

View file

@ -306,7 +306,10 @@ impl UI {
} }
pub async fn notifications(&self) -> Vec<Notification> { pub async fn notifications(&self) -> Vec<Notification> {
self.notifications.lock().map(|n| n.clone()).unwrap_or_default() self.notifications
.lock()
.map(|n| n.clone())
.unwrap_or_default()
} }
pub async fn set_screen(&self, screen: Box<dyn Screen>) { pub async fn set_screen(&self, screen: Box<dyn Screen>) {
@ -611,7 +614,9 @@ impl UI {
Ok(iota_ipc::ResponseResult::Error(error)) => { Ok(iota_ipc::ResponseResult::Error(error)) => {
Err(format!("Cannot load users: {error}")) Err(format!("Cannot load users: {error}"))
} }
Ok(_) => Err("Daemon returned an unexpected response while loading users.".into()), Ok(_) => {
Err("Daemon returned an unexpected response while loading users.".into())
}
Err(error) => Err(format!("Cannot load users: {error}")), Err(error) => Err(format!("Cannot load users: {error}")),
} }
}; };
@ -705,7 +710,12 @@ impl UI {
width: 40.min(rows[1].width), width: 40.min(rows[1].width),
height: 3.min(rows[1].height), height: 3.min(rows[1].height),
}; };
render_notification_area(f, notification_area, &notifications, context.theme); render_notification_area(
f,
notification_area,
&notifications,
context.theme,
);
} }
} }
})?; })?;

View file

@ -6,4 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
iota-storage = { path = "../iota-storage" } iota-storage = { path = "../iota-storage" }
iota-util = { path = "../iota-util" } iota-util = { path = "../iota-util" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["crypto"] }
[dev-dependencies]
tokio = { version = "1.50.0", features = ["macros", "rt"] }

View file

@ -1,3 +1,4 @@
pub mod connection_handler; pub mod connection_handler;
pub mod message_common; pub mod message_common;
pub mod message_handlers; pub mod message_handlers;
pub mod relay;

View file

@ -2,6 +2,8 @@ use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::type_map::TypeMap; use mtp::type_map::TypeMap;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
pub use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt};
pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue {
use mtp::type_map::{DataTypeId, TypeMap}; use mtp::type_map::{DataTypeId, TypeMap};
let tm = TypeMap::latest(); let tm = TypeMap::latest();
@ -15,22 +17,34 @@ pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue {
pub fn data_string(cv: &CommunicationValue, dt: DataType) -> Option<String> { pub fn data_string(cv: &CommunicationValue, dt: DataType) -> Option<String> {
cv.get_data(dt) cv.get_data(dt)
.as_str() .and_then(DataValue::as_str)
.map(|s| s.to_string()) .map(|s| s.to_string())
.or_else(|| cv.get_data(dt).as_number().map(|n| n.to_string())) .or_else(|| {
.or_else(|| cv.get_data(dt).as_signed_number().map(|n| n.to_string())) cv.get_data(dt)
.and_then(DataValue::as_number)
.map(|n| n.to_string())
})
.or_else(|| {
cv.get_data(dt)
.and_then(DataValue::as_signed_number)
.map(|n| n.to_string())
})
} }
pub fn data_i64(cv: &CommunicationValue, dt: DataType) -> Option<i64> { pub fn data_i64(cv: &CommunicationValue, dt: DataType) -> Option<i64> {
cv.get_data(dt) cv.get_data(dt)
.as_number() .and_then(DataValue::as_number)
.and_then(|n| i64::try_from(n).ok()) .and_then(|n| i64::try_from(n).ok())
.or_else(|| { .or_else(|| {
cv.get_data(dt) cv.get_data(dt)
.as_signed_number() .and_then(DataValue::as_signed_number)
.and_then(|n| i64::try_from(n).ok()) .and_then(|n| i64::try_from(n).ok())
}) })
.or_else(|| cv.get_data(dt).as_str().and_then(|s| s.parse::<i64>().ok())) .or_else(|| {
cv.get_data(dt)
.and_then(DataValue::as_str)
.and_then(|s| s.parse::<i64>().ok())
})
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -67,7 +81,7 @@ pub fn recipient_from_value(value: &DataValue) -> Option<ChatSecretRecipient> {
} }
pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option<Vec<ChatSecretRecipient>> { pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option<Vec<ChatSecretRecipient>> {
let recipients = cv.get_data(DataType::Recipients).as_array()?; let recipients = cv.get_data(DataType::Recipients)?.as_array()?;
let parsed = recipients let parsed = recipients
.iter() .iter()
.map(recipient_from_value) .map(recipient_from_value)
@ -80,49 +94,6 @@ pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option<Vec<ChatSecretR
} }
} }
pub fn set_chat_secret_cv_for_recipient(
source: &CommunicationValue,
recipient: &ChatSecretRecipient,
) -> CommunicationValue {
let recipient_value = typed_container(vec![
(DataType::UserId, DataValue::Str(recipient.user_id.clone())),
(
DataType::EncryptedSecret,
DataValue::Bytes(recipient.encrypted_secret.clone()),
),
(
DataType::KemCiphertext,
DataValue::Bytes(recipient.kem_ciphertext.clone()),
),
]);
CommunicationValue::new(CommunicationType::SetChatSecret)
.with_id(source.get_id())
.with_sender(source.get_sender())
.with_receiver(recipient.user_id.parse::<u64>().unwrap_or(0))
.add_typed_default(DataType::ChatId, source.get_data(DataType::ChatId).clone())
.add_typed_default(
DataType::SecretId,
source.get_data(DataType::SecretId).clone(),
)
.add_typed_default(
DataType::VersionNumber,
source.get_data(DataType::VersionNumber).clone(),
)
.add_typed_default(
DataType::WrappingScheme,
source.get_data(DataType::WrappingScheme).clone(),
)
.add_typed_default(
DataType::CreatedAt,
source.get_data(DataType::CreatedAt).clone(),
)
.add_typed_default(
DataType::Recipients,
DataValue::Array(vec![recipient_value]),
)
}
pub fn now_millis_i64() -> i64 { pub fn now_millis_i64() -> i64 {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -131,7 +102,9 @@ pub fn now_millis_i64() -> i64 {
} }
pub fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue { pub fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue {
CommunicationValue::new(ty) let mut response = CommunicationValue::new(ty).with_id(request.id().unwrap_or_default());
.with_id(request.get_id()) if let Some(sender) = request.sender() {
.with_receiver(request.get_sender()) response = response.with_receiver(sender);
}
response
} }

View file

@ -4,7 +4,11 @@ use iota_storage::util::chats_util::{self, get_user, mod_user};
use iota_storage::util::communities_util::CommunitiesUtil; use iota_storage::util::communities_util::CommunitiesUtil;
use iota_storage::util::e2ee_storage::{self, ChatSecretQuery}; use iota_storage::util::e2ee_storage::{self, ChatSecretQuery};
use iota_storage::util::settings; use iota_storage::util::settings;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::codec::{
CommunicationType, CommunicationValue, DataType, DataValue, TypeMap, VerifiedRelayContent,
};
use crate::relay::VerifiedRelayContext;
pub struct MessageMutation { pub struct MessageMutation {
pub sender_id: i64, pub sender_id: i64,
@ -33,6 +37,170 @@ pub fn success_response(cv: &CommunicationValue) -> CommunicationValue {
error_response(cv, CommunicationType::Success) error_response(cv, CommunicationType::Success)
} }
fn relay_field<'a>(
payload: &'a DataValue,
data_type: DataType,
type_map: &TypeMap,
) -> Option<&'a DataValue> {
payload.get_field(data_type.try_to_id(type_map)?)
}
fn relay_string<'a>(
payload: &'a DataValue,
data_type: DataType,
type_map: &TypeMap,
) -> Option<&'a str> {
relay_field(payload, data_type, type_map)?.as_str()
}
fn relay_number(payload: &DataValue, data_type: DataType, type_map: &TypeMap) -> Option<i128> {
relay_field(payload, data_type, type_map)?.as_number()
}
fn relay_identity(
payload: &DataValue,
data_type: DataType,
type_map: &TypeMap,
) -> Result<Option<u64>, String> {
let Some(value) = relay_field(payload, data_type, type_map) else {
return Ok(None);
};
if let Some(number) = value.as_number() {
return u64::try_from(number)
.map(Some)
.map_err(|_| format!("Relay {data_type:?} is outside the user ID range"));
}
if let Some(text) = value.as_str() {
return text
.parse::<u64>()
.map(Some)
.map_err(|_| format!("Relay {data_type:?} is not a user ID"));
}
Err(format!("Relay {data_type:?} has an invalid user ID value"))
}
fn validate_relay_identity(
context: &VerifiedRelayContext,
payload: &DataValue,
) -> Result<(), String> {
if relay_identity(payload, DataType::SenderId, &context.type_map)?
.is_some_and(|sender_id| sender_id != context.signer_id)
{
return Err("Relay SenderId does not match the authenticated signer".into());
}
if relay_identity(payload, DataType::ReceiverId, &context.type_map)?
.is_some_and(|receiver_id| receiver_id != context.final_recipient_id)
{
return Err("Relay ReceiverId does not match the authenticated recipient".into());
}
Ok(())
}
/*
* Apply only operations whose actor and recipient can be taken from verified
* Relay metadata. The raw Relay frame never enters these handlers, so outer
* routing fields cannot become application identity.
*/
pub fn apply_verified_relay_content(
context: &VerifiedRelayContext,
content: &VerifiedRelayContent,
) -> Result<(), String> {
validate_relay_identity(context, &content.content)?;
let sender_id = i64::try_from(context.signer_id)
.map_err(|_| "Relay signer ID exceeds the local storage range".to_string())?;
let recipient_id = i64::try_from(context.final_recipient_id)
.map_err(|_| "Relay recipient ID exceeds the local storage range".to_string())?;
let created_at = i64::try_from(context.created_at)
.map_err(|_| "Relay creation time exceeds the local storage range".to_string())?;
match content.message_type.as_str() {
"MessageSend" => {
let message = relay_string(&content.content, DataType::Content, &context.type_map)
.ok_or_else(|| "Relay MessageSend is missing Content".to_string())?;
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
.and_then(|value| i64::try_from(value).ok())
.unwrap_or(created_at);
let height = relay_number(&content.content, DataType::Height, &context.type_map)
.and_then(|value| i64::try_from(value).ok())
.unwrap_or_default();
let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map)
.and_then(|value| i64::try_from(value).ok());
chat_files::add_message(
u128::try_from(send_time)
.map_err(|_| "Relay MessageSend has a negative SendTime".to_string())?,
false,
recipient_id,
sender_id,
message,
height,
reply_to,
);
Ok(())
}
"MessageEdit" => {
let message = relay_string(&content.content, DataType::Content, &context.type_map)
.ok_or_else(|| "Relay MessageEdit is missing Content".to_string())?;
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
.and_then(|value| i64::try_from(value).ok())
.ok_or_else(|| "Relay MessageEdit is missing SendTime".to_string())?;
chat_files::apply_remote_edit(recipient_id, sender_id, send_time, sender_id, message)
.map_err(|error| error.to_string())
}
"MessageReactionAdd" | "MessageReactionRemove" => {
let reaction = relay_string(&content.content, DataType::Reaction, &context.type_map)
.filter(|value| !value.is_empty() && value.len() <= 64)
.ok_or_else(|| "Relay reaction is invalid".to_string())?;
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
.and_then(|value| i64::try_from(value).ok())
.ok_or_else(|| "Relay reaction is missing SendTime".to_string())?;
let result = if content.message_type == "MessageReactionAdd" {
chat_files::add_reaction(recipient_id, sender_id, send_time, sender_id, reaction)
} else {
chat_files::remove_reaction(recipient_id, sender_id, send_time, sender_id, reaction)
};
result.map_err(|error| error.to_string())
}
"MessageDeleteLive" => {
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
.and_then(|value| i64::try_from(value).ok())
.ok_or_else(|| "Relay MessageDeleteLive is missing SendTime".to_string())?;
chat_files::apply_remote_delete(recipient_id, sender_id, send_time, sender_id)
.map_err(|error| error.to_string())
}
"SetChatSecret" => {
let frame = CommunicationValue::new(CommunicationType::SetChatSecret)
.with_payload(content.content.clone());
let recipients = chat_secret_recipients(&frame)
.ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?;
let recipient = recipients
.into_iter()
.find(|value| value.user_id == context.final_recipient_id.to_string())
.ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?;
let chat_id = data_string(&frame, DataType::ChatId)
.ok_or_else(|| "Relay SetChatSecret is missing ChatId".to_string())?;
let secret_id = data_string(&frame, DataType::SecretId)
.ok_or_else(|| "Relay SetChatSecret is missing SecretId".to_string())?;
let version = data_i64(&frame, DataType::VersionNumber)
.ok_or_else(|| "Relay SetChatSecret is missing VersionNumber".to_string())?;
let wrapping_scheme = data_string(&frame, DataType::WrappingScheme)
.ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?;
e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret {
user_id: context.final_recipient_id.to_string(),
chat_id,
secret_id,
version,
encrypted_secret: recipient.encrypted_secret,
kem_ciphertext: recipient.kem_ciphertext,
wrapping_scheme,
created_at,
updated_at: now_millis_i64(),
})
.map_err(|error| error.to_string())
}
_ => Ok(()),
}
}
pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue { pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue {
let mutation = match message_mutation(cv) { let mutation = match message_mutation(cv) {
Ok(mutation) => mutation, Ok(mutation) => mutation,
@ -346,7 +514,9 @@ mod presence_tests {
fn sync_error(cv: &CommunicationValue) -> CommunicationValue { fn sync_error(cv: &CommunicationValue) -> CommunicationValue {
error_response(cv, CommunicationType::ErrorInvalidData).add_typed_default( error_response(cv, CommunicationType::ErrorInvalidData).add_typed_default(
DataType::SessionId, DataType::SessionId,
cv.get_data(DataType::SessionId).clone(), cv.get_data(DataType::SessionId)
.cloned()
.unwrap_or(DataValue::Null),
) )
} }

View file

@ -0,0 +1,350 @@
use iota_util::route_target::RouteTarget;
use mtp::codec::{
CommunicationValue, ProtectionPolicy, RelayError, SignaturePolicy, TypeMap,
VerifiedRelayContent, VerifiedRelayMetadata, forward_relay_frame,
open_relay_content_with_keyrings, open_relay_metadata_with, relay_metadata_claimed_signer_id,
};
use mtp::crypto::{Keyring, PublicKeyBundle};
use std::fmt;
pub const RELAY_PROTECTION_POLICY: ProtectionPolicy = ProtectionPolicy {
signature: SignaturePolicy::Dual,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageSecurityClass {
RelayOnly,
AuthenticatedPeerControl,
AuthenticatedLocalRequest,
}
pub fn message_security_class(frame: &CommunicationValue) -> MessageSecurityClass {
const RELAY_ONLY_TYPES: &[mtp::codec::CommunicationType] = &[
mtp::codec::CommunicationType::MessageSend,
mtp::codec::CommunicationType::MessageLive,
mtp::codec::CommunicationType::MessageState,
mtp::codec::CommunicationType::MessageEdit,
mtp::codec::CommunicationType::MessageEditLive,
mtp::codec::CommunicationType::MessageReactionAdd,
mtp::codec::CommunicationType::MessageReactionRemove,
mtp::codec::CommunicationType::MessageReactionLive,
mtp::codec::CommunicationType::MessageDelete,
mtp::codec::CommunicationType::MessageDeleteLive,
mtp::codec::CommunicationType::MessageOtherIota,
mtp::codec::CommunicationType::SetChatSecret,
mtp::codec::CommunicationType::SendChat,
mtp::codec::CommunicationType::SettingsSave,
mtp::codec::CommunicationType::GlobalSettingsSave,
mtp::codec::CommunicationType::AddConversation,
mtp::codec::CommunicationType::AddCommunity,
mtp::codec::CommunicationType::RemoveCommunity,
];
if RELAY_ONLY_TYPES.iter().any(|kind| frame.is_type(*kind)) {
MessageSecurityClass::RelayOnly
} else if frame.is_type(mtp::codec::CommunicationType::GetChatSecret)
|| frame.is_type(mtp::codec::CommunicationType::MessageGet)
|| frame.is_type(mtp::codec::CommunicationType::MessagesGet)
{
MessageSecurityClass::AuthenticatedPeerControl
} else {
MessageSecurityClass::AuthenticatedLocalRequest
}
}
#[derive(Debug, Clone)]
pub struct UserIdentity {
pub user_id: u64,
pub iota_id: u64,
pub signing_keys: Vec<PublicKeyBundle>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedRelayContext {
pub signer_id: u64,
pub final_recipient_id: u64,
pub message_id: String,
pub created_at: u64,
pub type_map: TypeMap,
}
#[derive(Debug, Clone)]
pub struct VerifiedRelay {
pub metadata: VerifiedRelayMetadata,
pub context: VerifiedRelayContext,
pub signing_keys: Vec<PublicKeyBundle>,
}
#[derive(Debug)]
pub enum RelayValidationError {
WrongNextHop { expected: u64, actual: Option<u64> },
OuterSenderNotAllowed,
MissingSigningKeys(u64),
MissingTypeMap,
InvalidRouteTarget(u64),
KeyLookup(String),
Relay(RelayError),
}
impl fmt::Display for RelayValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WrongNextHop { expected, actual } => {
write!(
formatter,
"relay next hop {:?} does not match Iota {expected}",
actual
)
}
Self::OuterSenderNotAllowed => formatter.write_str("relay has an outer sender"),
Self::MissingSigningKeys(signer_id) => {
write!(formatter, "no trusted signing keys for user {signer_id}")
}
Self::MissingTypeMap => formatter.write_str("relay has no negotiated type map"),
Self::InvalidRouteTarget(target) => {
write!(formatter, "relay has invalid route target {target}")
}
Self::KeyLookup(error) => write!(formatter, "trusted signer lookup failed: {error}"),
Self::Relay(error) => error.fmt(formatter),
}
}
}
impl std::error::Error for RelayValidationError {}
impl From<RelayError> for RelayValidationError {
fn from(error: RelayError) -> Self {
Self::Relay(error)
}
}
/*
* Relay metadata is opened only after the claimed signer selects trusted key
* history. Replay reservation happens after verification and durable
* acceptance, so a failed delivery can be retried without losing the frame.
*/
pub async fn verify_relay_metadata<F, Fut>(
frame: &CommunicationValue,
local_iota_id: u64,
keyring: &Keyring,
resolve_signing_keys: F,
) -> Result<VerifiedRelay, RelayValidationError>
where
F: FnOnce(u64) -> Fut,
Fut: Future<Output = Result<Vec<PublicKeyBundle>, RelayValidationError>>,
{
let expected_next_hop = RouteTarget::Iota(local_iota_id)
.wire_id()
.ok_or(RelayValidationError::InvalidRouteTarget(local_iota_id))?;
if frame.receiver() != Some(expected_next_hop) {
return Err(RelayValidationError::WrongNextHop {
expected: expected_next_hop,
actual: frame.receiver(),
});
}
if frame.sender().is_some() {
return Err(RelayValidationError::OuterSenderNotAllowed);
}
let claimed_signer = relay_metadata_claimed_signer_id(frame, &[keyring])?;
let signing_keys = resolve_signing_keys(claimed_signer).await?;
if signing_keys.is_empty() {
return Err(RelayValidationError::MissingSigningKeys(claimed_signer));
}
let resolver_keys = signing_keys.clone();
let type_map = frame
.type_map()
.cloned()
.ok_or(RelayValidationError::MissingTypeMap)?;
let metadata = open_relay_metadata_with(
frame,
&[keyring],
Some(claimed_signer),
move |signer_id| (signer_id == claimed_signer).then(|| resolver_keys.clone()),
RELAY_PROTECTION_POLICY,
None,
)?;
let context = VerifiedRelayContext {
signer_id: metadata.signer_id(),
final_recipient_id: metadata.final_recipient_id(),
message_id: metadata.message_id().to_owned(),
created_at: metadata.created_at(),
type_map,
};
Ok(VerifiedRelay {
metadata,
context,
signing_keys,
})
}
pub fn open_verified_relay_content(
relay: &VerifiedRelay,
keyrings: &[&Keyring],
expected_recipient_id: u64,
) -> Result<VerifiedRelayContent, RelayValidationError> {
Ok(open_relay_content_with_keyrings(
&relay.metadata,
keyrings,
&relay.signing_keys,
Some(expected_recipient_id),
RELAY_PROTECTION_POLICY,
)?)
}
pub fn forward_verified_relay(
frame: &CommunicationValue,
target: RouteTarget,
) -> Result<CommunicationValue, RelayValidationError> {
let next_hop_id = target
.wire_id()
.ok_or(RelayValidationError::InvalidRouteTarget(target.id()))?;
Ok(forward_relay_frame(frame, next_hop_id)?)
}
#[cfg(test)]
mod tests {
use super::*;
use mtp::codec::SealedRelayBuilder;
use mtp::crypto::{DualSigner, Ed25519Signer, Keyring};
fn relay(message_id: &str) -> Result<(Keyring, Keyring, CommunicationValue), String> {
let signer_keyring = Keyring::generate();
let recipient_keyring = Keyring::generate();
let signer = DualSigner::new(
&signer_keyring.sig_cl_secret_key,
&signer_keyring.sig_pq_secret_key,
&signer_keyring.sig_pq_public_key,
)
.map_err(|error| error.to_string())?;
let frame = SealedRelayBuilder::new(
"MessageSend",
mtp::codec::DataValue::Str("payload".into()),
7,
42,
RouteTarget::Iota(99)
.wire_id()
.ok_or("invalid test target")?,
&signer,
)
.message_id(message_id)
.created_at(123)
.metadata_recipients(vec![recipient_keyring.public_key_bundle()])
.content_recipients(vec![recipient_keyring.public_key_bundle()])
.build()
.map_err(|error| error.to_string())?;
Ok((signer_keyring, recipient_keyring, frame))
}
#[tokio::test]
async fn verifies_metadata_with_trusted_signing_key() -> Result<(), String> {
let (signer, recipient, frame) = relay("accepted")?;
let trusted_key = signer.public_key_bundle();
let verified = verify_relay_metadata(&frame, 99, &recipient, move |signer_id| async move {
(signer_id == 7)
.then_some(vec![trusted_key])
.ok_or(RelayValidationError::MissingSigningKeys(signer_id))
})
.await
.map_err(|error| error.to_string())?;
assert_eq!(verified.context.signer_id, 7);
assert_eq!(verified.context.final_recipient_id, 42);
assert_eq!(verified.context.message_id, "accepted");
Ok(())
}
#[tokio::test]
async fn rejects_metadata_signed_by_untrusted_key() -> Result<(), String> {
let (_signer, recipient, frame) = relay("wrong-key")?;
let wrong_signer = Keyring::generate();
let trusted_key = wrong_signer.public_key_bundle();
let result = verify_relay_metadata(&frame, 99, &recipient, move |_| async move {
Ok(vec![trusted_key])
})
.await;
assert!(matches!(result, Err(RelayValidationError::Relay(_))));
Ok(())
}
#[tokio::test]
async fn rejects_classical_only_relay_under_dual_policy() -> Result<(), String> {
let signer_keyring = Keyring::generate();
let recipient_keyring = Keyring::generate();
let signer = Ed25519Signer::new(&signer_keyring.sig_cl_secret_key)
.map_err(|error| error.to_string())?;
let frame = SealedRelayBuilder::new(
"MessageSend",
mtp::codec::DataValue::Str("payload".into()),
7,
42,
RouteTarget::Iota(99)
.wire_id()
.ok_or("invalid test target")?,
&signer,
)
.message_id("classical-only")
.created_at(123)
.metadata_recipients(vec![recipient_keyring.public_key_bundle()])
.content_recipients(vec![recipient_keyring.public_key_bundle()])
.build()
.map_err(|error| error.to_string())?;
let trusted_key = signer_keyring.public_key_bundle();
let result = verify_relay_metadata(&frame, 99, &recipient_keyring, move |_| async move {
Ok(vec![trusted_key])
})
.await;
assert!(matches!(result, Err(RelayValidationError::Relay(_))));
Ok(())
}
#[tokio::test]
async fn rejects_outer_sender_before_key_lookup() -> Result<(), String> {
let (_signer, recipient, frame) = relay("outer-sender")?;
let frame = frame.with_sender(501);
let result = verify_relay_metadata(&frame, 99, &recipient, |_| async {
Err(RelayValidationError::MissingSigningKeys(7))
})
.await;
assert!(matches!(
result,
Err(RelayValidationError::OuterSenderNotAllowed)
));
Ok(())
}
#[tokio::test]
async fn verification_does_not_commit_replay_state() -> Result<(), String> {
let (signer, recipient, frame) = relay("duplicate")?;
let trusted_key = signer.public_key_bundle();
for _ in 0..2 {
let trusted_key = trusted_key.clone();
let result = verify_relay_metadata(&frame, 99, &recipient, move |_| async move {
Ok(vec![trusted_key])
})
.await;
let _ = result.map_err(|error| error.to_string())?;
}
Ok(())
}
#[test]
fn forwarding_preserves_sealed_payload() -> Result<(), String> {
let (_signer, _recipient, frame) = relay("forwarding")?;
let forwarded = forward_verified_relay(&frame, RouteTarget::User(100))
.map_err(|error| error.to_string())?;
assert_eq!(frame.sender(), None);
assert_eq!(forwarded.sender(), None);
assert_eq!(forwarded.receiver(), RouteTarget::User(100).wire_id());
assert_eq!(frame.payload(), forwarded.payload());
Ok(())
}
}

View file

@ -100,10 +100,25 @@ impl CommandRouter {
.into_iter() .into_iter()
.map(|user| UserSummary { .map(|user| UserSummary {
credential_present: user.state == user_manager::LocalUserState::Managed credential_present: user.state == user_manager::LocalUserState::Managed
&& user_manager::get_user(user.user_id).is_some_and(|profile| iota_util::file_util::read_user_credential_with_legacy(user.user_id, &profile.username).ok().flatten().is_some()), && user_manager::get_user(user.user_id).is_some_and(|profile| {
iota_util::file_util::read_user_credential_with_legacy(
user.user_id,
&profile.username,
)
.ok()
.flatten()
.is_some()
}),
user_id: user.user_id, user_id: user.user_id,
username: user.username, username: user.username,
state: match user.state { user_manager::LocalUserState::Managed => iota_ipc::LocalUserState::Managed, user_manager::LocalUserState::Released => iota_ipc::LocalUserState::Released }, state: match user.state {
user_manager::LocalUserState::Managed => {
iota_ipc::LocalUserState::Managed
}
user_manager::LocalUserState::Released => {
iota_ipc::LocalUserState::Released
}
},
data_present: user.data_present, data_present: user.data_present,
}) })
.collect(); .collect();
@ -145,7 +160,8 @@ impl CommandRouter {
} }
} }
} }
LocalRequest::PurgeUserData { user_id } => match user_manager::purge_user_data(user_id) { LocalRequest::PurgeUserData { user_id } => match user_manager::purge_user_data(user_id)
{
Ok(()) => ResponseResult::Ok(ResponsePayload::UserDataPurged { user_id }), Ok(()) => ResponseResult::Ok(ResponsePayload::UserDataPurged { user_id }),
Err(error) => { Err(error) => {
log!("User data purge failed for {user_id}: {error}"); log!("User data purge failed for {user_id}: {error}");
@ -153,25 +169,51 @@ impl CommandRouter {
} }
}, },
LocalRequest::AttachUserFromTu { credential } => { LocalRequest::AttachUserFromTu { credential } => {
match omikron_connector::user_ops::attach_user_from_tu(self.services.omikron.as_ref(), &credential.0).await { match omikron_connector::user_ops::attach_user_from_tu(
Ok(user) => ResponseResult::Ok(ResponsePayload::Acknowledged { message: format!("Added {} ({}) to this Iota", user.username, user.user_id) }), 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) => { Err(error) => {
log!("Credential attach failed: {error:?}"); log!("Credential attach failed: {error:?}");
ResponseResult::Error(IpcErrorCode::Unauthorized) ResponseResult::Error(IpcErrorCode::Unauthorized)
} }
} }
} }
LocalRequest::CompleteDeleteUser { user_id, credential } => { LocalRequest::CompleteDeleteUser {
user_id,
credential,
} => {
let contents = match credential { let contents = match credential {
Some(value) => Ok(value.0), Some(value) => Ok(value.0),
None => user_manager::get_user(user_id) None => user_manager::get_user(user_id)
.ok_or(()) .ok_or(())
.and_then(|user| iota_util::file_util::read_user_credential_with_legacy(user_id, &user.username).map_err(|_| ())) .and_then(|user| {
iota_util::file_util::read_user_credential_with_legacy(
user_id,
&user.username,
)
.map_err(|_| ())
})
.and_then(|value| value.ok_or(())), .and_then(|value| value.ok_or(())),
}; };
let Ok(contents) = contents else { return ResponseResult::Error(IpcErrorCode::Unauthorized); }; let Ok(contents) = contents else {
match omikron_connector::user_ops::complete_delete_user_with_tu(self.services.omikron.as_ref(), &contents, user_id).await { return ResponseResult::Error(IpcErrorCode::Unauthorized);
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { message: format!("Deleted Tensamin account {user_id}") }), };
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) => { Err(error) => {
log!("Credential deletion failed for {user_id}: {error:?}"); log!("Credential deletion failed for {user_id}: {error:?}");
ResponseResult::Error(IpcErrorCode::Unauthorized) ResponseResult::Error(IpcErrorCode::Unauthorized)
@ -185,19 +227,34 @@ impl CommandRouter {
} }
let request = CommunicationValue::new(CommunicationType::ReleaseUserFromIota) let request = CommunicationValue::new(CommunicationType::ReleaseUserFromIota)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())); .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
match self.services.omikron.await_response(&request, Duration::from_secs(20)).await { match self
Ok(response) if response.is_type(CommunicationType::Success) => match user_manager::release_user(user_id) { .services
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { .omikron
message: format!("Released user {user_id}; hosted data was retained"), .await_response(&request, Duration::from_secs(20))
}), .await
Err(error) => { {
log!("Remote release succeeded but local cleanup failed for {user_id}: {error}"); Ok(response) if response.is_type(CommunicationType::Success) => {
ResponseResult::Error(IpcErrorCode::StorageFailure) match user_manager::release_user(user_id) {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!(
"Released user {user_id}; hosted data was retained"
),
}),
Err(error) => {
log!(
"Remote release succeeded but local cleanup failed for {user_id}: {error}"
);
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
} }
}, }
Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => ResponseResult::Error(IpcErrorCode::Unauthorized), Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => {
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
Ok(_) => ResponseResult::Error(IpcErrorCode::Conflict), Ok(_) => ResponseResult::Error(IpcErrorCode::Conflict),
Err(omikron_connector::OmikronError::Timeout(_)) => ResponseResult::Error(IpcErrorCode::Timeout), Err(omikron_connector::OmikronError::Timeout(_)) => {
ResponseResult::Error(IpcErrorCode::Timeout)
}
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable), Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
} }
} }
@ -294,18 +351,28 @@ impl CommandRouter {
} }
LocalRequest::GetUser { user_id } => match user_manager::get_user(user_id) { LocalRequest::GetUser { user_id } => match user_manager::get_user(user_id) {
Some(user) => { Some(user) => {
let credential_present = iota_util::file_util::read_user_credential_with_legacy(user_id, &user.username).ok().flatten().is_some(); let credential_present =
iota_util::file_util::read_user_credential_with_legacy(
user_id,
&user.username,
)
.ok()
.flatten()
.is_some();
ResponseResult::Ok(ResponsePayload::UserDetail(UserDetailResponse { ResponseResult::Ok(ResponsePayload::UserDetail(UserDetailResponse {
user_id: user.user_id, user_id: user.user_id,
username: user.username, username: user.username,
display_name: user.display_name, display_name: user.display_name,
created_at: user.created_at, created_at: user.created_at,
trusted_apps: user.trusted_apps.keys().cloned().collect(), trusted_apps: user.trusted_apps.keys().cloned().collect(),
state: iota_ipc::LocalUserState::Managed, 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), data_present: user_manager::get_residency()
credential_present, .iter()
})) .find(|entry| entry.user_id == user_id)
}, .is_none_or(|entry| entry.data_present),
credential_present,
}))
}
None => ResponseResult::Error(IpcErrorCode::NotFound), None => ResponseResult::Error(IpcErrorCode::NotFound),
}, },
LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest),

View file

@ -215,8 +215,14 @@ async fn main() -> ExitCode {
.spawn_tracked("user-lifecycle-reconciliation", async move { .spawn_tracked("user-lifecycle-reconciliation", async move {
let mut states = omikron_reconcile.connection_state(); let mut states = omikron_reconcile.connection_state();
loop { loop {
if matches!(*states.borrow(), omikron_connector::omikron_connection::ConnectionState::Connected { .. }) { if matches!(
omikron_connector::user_ops::reconcile_managed_users(omikron_reconcile.as_ref()).await; *states.borrow(),
omikron_connector::omikron_connection::ConnectionState::Connected { .. }
) {
omikron_connector::user_ops::reconcile_managed_users(
omikron_reconcile.as_ref(),
)
.await;
} }
tokio::select! { tokio::select! {
changed = states.changed() => if changed.is_err() { break }, changed = states.changed() => if changed.is_err() { break },

View file

@ -6,9 +6,10 @@ pub use protocol::{
ClientMessage, CommunitySummary, ComponentHealth, ComponentId, ComponentStatusResponse, ClientMessage, CommunitySummary, ComponentHealth, ComponentId, ComponentStatusResponse,
ConfigResponse, ConnectionStatus, DaemonMessage, DaemonStatusResponse, DeploymentMode, ConfigResponse, ConnectionStatus, DaemonMessage, DaemonStatusResponse, DeploymentMode,
ExitIntent, HealthStatus, HelloAck, IpcErrorCode, LifecycleEvent, LifecyclePhase, LocalRequest, ExitIntent, HealthStatus, HelloAck, IpcErrorCode, LifecycleEvent, LifecyclePhase, LocalRequest,
LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse, RequestEnvelope, LocalUserState, LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse,
ResponseEnvelope, ResponsePayload, ResponseResult, StartupPhase, StateSnapshot, StatusResponse, RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult, SecretString, StartupPhase,
SecretString, SupervisorKind, TaskSummary, UpdateStatusResponse, UserDetailResponse, UserSummary, LocalUserState, StateSnapshot, StatusResponse, SupervisorKind, TaskSummary, UpdateStatusResponse,
UserDetailResponse, UserSummary,
}; };
pub use transport::{read_msg, write_msg}; pub use transport::{read_msg, write_msg};

View file

@ -163,11 +163,20 @@ pub enum ResponsePayload {
Status(StatusResponse), Status(StatusResponse),
Tasks(Vec<TaskSummary>), Tasks(Vec<TaskSummary>),
Users(Vec<UserSummary>), Users(Vec<UserSummary>),
UserCreated { user_id: i64, username: String }, UserCreated {
user_id: i64,
username: String,
},
/// Retained only for wire compatibility. New lifecycle code never emits it. /// Retained only for wire compatibility. New lifecycle code never emits it.
UserRemoved { user_id: i64 }, UserRemoved {
UserDataPurged { user_id: i64 }, user_id: i64,
Acknowledged { message: String }, },
UserDataPurged {
user_id: i64,
},
Acknowledged {
message: String,
},
DaemonStatus(DaemonStatusResponse), DaemonStatus(DaemonStatusResponse),
Config(ConfigResponse), Config(ConfigResponse),
OmikronStatus(OmikronStatusResponse), OmikronStatus(OmikronStatusResponse),

View file

@ -318,28 +318,29 @@ pub fn log_cv_internal(
pub fn format_cv(cv: &CommunicationValue) -> String { pub fn format_cv(cv: &CommunicationValue) -> String {
let mut parts = Vec::new(); let mut parts = Vec::new();
let sender = cv.get_sender(); match (cv.sender(), cv.receiver()) {
let receiver = cv.get_receiver(); (Some(sender), Some(receiver)) => parts.push(format!("{} > {}", sender, receiver)),
(Some(sender), None) => parts.push(sender.to_string()),
if sender > 0 && receiver > 0 { (None, Some(receiver)) => parts.push(format!("> {}", receiver)),
parts.push(format!("{} > {}", sender, receiver)); (None, None) => {}
} else if sender > 0 {
parts.push(format!("{}", sender));
} else if receiver > 0 {
parts.push(format!("> {}", receiver));
} }
let comm_type = cv let comm_type = cv
.get_comm_type_enum() .get_comm_type_enum()
.map(|kind| kind.to_string()) .map(|kind| kind.to_string())
.unwrap_or_else(|| cv.get_type().to_string()); .unwrap_or_else(|| cv.get_type().to_string());
parts.push(format!("{} (id={})", comm_type, cv.get_id())); let id = cv
.id()
.map_or_else(|| "none".to_string(), |value| value.to_string());
parts.push(format!("{} (id={})", comm_type, id));
let data = cv.data(); let version = cv
.type_map()
let formated_data = format_data_container( .map(|type_map| type_map.version.clone())
data.iter().map(|(k, v)| (*k, v.clone())).collect(), .unwrap_or_else(|| Version(3, 0));
Version(1, 0), let formated_data = cv.data().map_or_else(
|| "<opaque payload>".to_string(),
|data| format_data_container(data.to_vec(), version),
); );
parts.push(format!("{}", formated_data)); parts.push(format!("{}", formated_data));

View file

@ -19,7 +19,10 @@ pub struct UserResidency {
} }
fn now_millis() -> i64 { fn now_millis() -> i64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as i64 SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
} }
pub fn add_user(user: UserProfile) { pub fn add_user(user: UserProfile) {
@ -235,7 +238,10 @@ pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageErr
})?; })?;
db::with_db(|conn| { db::with_db(|conn| {
let tx = conn.unchecked_transaction()?; let tx = conn.unchecked_transaction()?;
tx.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?; tx.execute(
"DELETE FROM trusted_apps WHERE user_id = ?1",
params![user_id],
)?;
tx.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?; tx.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?;
tx.execute( tx.execute(
r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at) r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at)
@ -246,7 +252,8 @@ pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageErr
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
})?; })?;
remove_user_credential(user_id).map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) remove_user_credential(user_id)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))
} }
/// Authoritative hosted-data erasure used by local purge and future Omega /// Authoritative hosted-data erasure used by local purge and future Omega
@ -256,14 +263,35 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage
let tx = conn.unchecked_transaction()?; 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 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 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(
tx.execute("DELETE FROM contacts WHERE storage_owner = ?1", params![user_id])?; "DELETE FROM messages WHERE storage_owner = ?1",
tx.execute("DELETE FROM communities WHERE storage_owner = ?1", params![user_id])?; 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 settings WHERE user_id = ?1", params![user_id])?;
tx.execute("DELETE FROM sync_events WHERE user_id = ?1", params![user_id])?; tx.execute(
tx.execute("DELETE FROM sync_heads WHERE user_id = ?1", params![user_id])?; "DELETE FROM sync_events WHERE user_id = ?1",
tx.execute("DELETE FROM client_sync_state WHERE user_id = ?1", params![user_id])?; params![user_id],
tx.execute("DELETE FROM trusted_apps 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( tx.execute(
"UPDATE user_residency SET data_state = 'empty', updated_at = ?2 WHERE user_id = ?1", "UPDATE user_residency SET data_state = 'empty', updated_at = ?2 WHERE user_id = ?1",
params![user_id, now_millis()], params![user_id, now_millis()],
@ -273,7 +301,8 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage
})?; })?;
crate::util::e2ee_storage::purge_user(user_id) crate::util::e2ee_storage::purge_user(user_id)
.map_err(crate::storage_error::StorageError::Other)?; .map_err(crate::storage_error::StorageError::Other)?;
delete_user_directory(user_id).map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) delete_user_directory(user_id)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))
} }
/// Complete local erasure is idempotent and is the target for a durable /// Complete local erasure is idempotent and is the target for a durable
@ -281,12 +310,19 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage
pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::StorageError> { pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::StorageError> {
purge_user_data(user_id)?; purge_user_data(user_id)?;
db::with_db(|conn| { db::with_db(|conn| {
conn.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?; conn.execute(
"DELETE FROM trusted_apps WHERE user_id = ?1",
params![user_id],
)?;
conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?; conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?;
conn.execute("DELETE FROM user_residency WHERE user_id = ?1", params![user_id])?; conn.execute(
"DELETE FROM user_residency WHERE user_id = ?1",
params![user_id],
)?;
Ok(()) Ok(())
})?; })?;
remove_user_credential(user_id).map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) remove_user_credential(user_id)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))
} }
pub fn get_residency() -> Vec<UserResidency> { pub fn get_residency() -> Vec<UserResidency> {
@ -306,14 +342,15 @@ pub fn get_residency() -> Vec<UserResidency> {
pub fn clear() { pub fn clear() {
if let Err(e) = db::with_db(|conn| { if let Err(e) = db::with_db(|conn| {
conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users; DELETE FROM user_residency;")?; conn.execute_batch(
"DELETE FROM trusted_apps; DELETE FROM users; DELETE FROM user_residency;",
)?;
Ok(()) Ok(())
}) { }) {
eprintln!("Failed to clear users: {}", e); eprintln!("Failed to clear users: {}", e);
} }
} }
pub fn save_users() { pub fn save_users() {
// No-op: users are auto-saved via SQLite. // No-op: users are auto-saved via SQLite.
} }

View file

@ -77,12 +77,22 @@ fn add_column_if_missing(
column: &str, column: &str,
definition: &str, definition: &str,
) -> Result<(), StorageError> { ) -> Result<(), StorageError> {
let mut statement = add_table_column_if_missing(conn, "messages", column, definition)
conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; }
fn add_table_column_if_missing(
conn: &Connection,
table: &str,
column: &str,
definition: &str,
) -> Result<(), StorageError> {
let mut statement = conn.prepare(&format!(
"SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?1"
))?;
let exists = statement.exists([column])?; let exists = statement.exists([column])?;
if !exists { if !exists {
conn.execute_batch(&format!("ALTER TABLE messages ADD COLUMN {definition};"))?; conn.execute_batch(&format!("ALTER TABLE {table} ADD COLUMN {definition};"))?;
} }
Ok(()) Ok(())
@ -266,6 +276,72 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
)?; )?;
} }
if current_version < 8 {
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS relay_replay (
signer_id INTEGER NOT NULL,
message_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (signer_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_relay_replay_created_at
ON relay_replay (created_at);
CREATE TABLE IF NOT EXISTS pending_relays (
id INTEGER PRIMARY KEY AUTOINCREMENT,
destination_id INTEGER NOT NULL,
target_kind INTEGER NOT NULL DEFAULT 0,
frame BLOB NOT NULL,
created_at INTEGER NOT NULL,
frame_id INTEGER NOT NULL DEFAULT 0,
UNIQUE(destination_id, frame)
);
CREATE INDEX IF NOT EXISTS idx_pending_relays_destination
ON pending_relays (destination_id, id);
PRAGMA user_version = 8;
"#,
)?;
}
if current_version < 9 {
add_table_column_if_missing(
conn,
"pending_relays",
"target_kind",
"target_kind INTEGER NOT NULL DEFAULT 0",
)?;
add_table_column_if_missing(
conn,
"pending_relays",
"type_map_version",
"type_map_version TEXT NOT NULL DEFAULT '1.0'",
)?;
add_table_column_if_missing(
conn,
"pending_relays",
"frame_id",
"frame_id INTEGER NOT NULL DEFAULT 0",
)?;
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS relay_inbox (
signer_id INTEGER NOT NULL,
message_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
frame BLOB NOT NULL,
type_map_version TEXT NOT NULL,
frame_id INTEGER NOT NULL,
state TEXT NOT NULL CHECK (state IN ('received', 'applied', 'queued', 'delivered', 'rejected')),
PRIMARY KEY (signer_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_relay_inbox_state
ON relay_inbox (state, created_at);
PRAGMA user_version = 9;
"#,
)?;
}
Ok(()) Ok(())
} }
@ -335,7 +411,7 @@ 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))?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 7); assert_eq!(version, 9);
for column in ["height", "reply_to", "edited_count", "deleted_by_external"] { for column in ["height", "reply_to", "edited_count", "deleted_by_external"] {
let mut statement = let mut statement =
conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?;
@ -352,8 +428,16 @@ mod tests {
run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?;
run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 7); assert_eq!(version, 9);
for table in ["sync_heads", "sync_events", "client_sync_state", "user_residency"] { for table in [
"sync_heads",
"sync_events",
"client_sync_state",
"user_residency",
"relay_replay",
"pending_relays",
"relay_inbox",
] {
let exists: i64 = conn.query_row( let exists: i64 = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1", "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
[table], [table],
@ -361,6 +445,11 @@ mod tests {
)?; )?;
assert_eq!(exists, 1); assert_eq!(exists, 1);
} }
for column in ["frame_id", "target_kind", "type_map_version"] {
let mut statement =
conn.prepare("SELECT 1 FROM pragma_table_info('pending_relays') WHERE name = ?1")?;
assert!(statement.exists([column])?);
}
Ok(()) Ok(())
} }
} }

View file

@ -24,19 +24,6 @@ pub struct ChatSecretQuery {
pub secret_id: Option<String>, pub secret_id: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingChatSecretForward {
pub recipient_user_id: String,
pub chat_id: String,
pub sender_user_id: String,
pub secret_id: String,
pub version: i64,
pub encrypted_secret: Vec<u8>,
pub kem_ciphertext: Vec<u8>,
pub wrapping_scheme: String,
pub created_at: i64,
}
static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| { static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
db::create_shared_connection( db::create_shared_connection(
"e2ee", "e2ee",
@ -63,21 +50,6 @@ static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
CREATE INDEX IF NOT EXISTS idx_chat_secrets_owner CREATE INDEX IF NOT EXISTS idx_chat_secrets_owner
ON chat_secrets (user_id, chat_id, secret_id); ON chat_secrets (user_id, chat_id, secret_id);
CREATE TABLE IF NOT EXISTS pending_chat_secret_forwards (
recipient_user_id TEXT NOT NULL,
chat_id TEXT NOT NULL,
sender_user_id TEXT NOT NULL,
secret_id TEXT NOT NULL,
version INTEGER NOT NULL,
encrypted_secret BLOB NOT NULL,
kem_ciphertext BLOB NOT NULL,
wrapping_scheme TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (recipient_user_id, chat_id, secret_id)
);
CREATE INDEX IF NOT EXISTS idx_pending_chat_secret_forwards_recipient
ON pending_chat_secret_forwards (recipient_user_id, created_at);
"#, "#,
) )
.expect("Failed to create or initialize E2EE DB") .expect("Failed to create or initialize E2EE DB")
@ -115,89 +87,15 @@ pub fn put_chat_secret(record: StoredChatSecret) -> Result<(), StorageError> {
}) })
} }
pub fn put_pending_chat_secret_forward( /// Erase every E2EE record owned by a user. The operation is
record: PendingChatSecretForward,
) -> Result<(), StorageError> {
db::with_conn(&E2EE_DB, |conn| {
conn.execute(
r#"
INSERT INTO pending_chat_secret_forwards (
recipient_user_id, chat_id, sender_user_id, secret_id, version,
encrypted_secret, kem_ciphertext, wrapping_scheme, created_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
ON CONFLICT(recipient_user_id, chat_id, secret_id) DO UPDATE SET
sender_user_id = excluded.sender_user_id,
version = excluded.version,
encrypted_secret = excluded.encrypted_secret,
kem_ciphertext = excluded.kem_ciphertext,
wrapping_scheme = excluded.wrapping_scheme,
created_at = excluded.created_at
"#,
params![
record.recipient_user_id,
record.chat_id,
record.sender_user_id,
record.secret_id,
record.version,
record.encrypted_secret,
record.kem_ciphertext,
record.wrapping_scheme,
record.created_at,
],
)?;
Ok(())
})
}
pub fn get_pending_chat_secret_forwards(
limit: i64,
) -> Result<Vec<PendingChatSecretForward>, StorageError> {
db::with_conn(&E2EE_DB, |conn| {
let mut stmt = conn.prepare(
r#"
SELECT recipient_user_id, chat_id, sender_user_id, secret_id, version,
encrypted_secret, kem_ciphertext, wrapping_scheme, created_at
FROM pending_chat_secret_forwards
ORDER BY created_at ASC
LIMIT ?1
"#,
)?;
let rows = stmt.query_map(params![limit.clamp(1, 500)], pending_forward_from_row)?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
Ok(out)
})
}
pub fn delete_pending_chat_secret_forward(
recipient_user_id: &str,
chat_id: &str,
secret_id: &str,
) -> Result<(), StorageError> {
db::with_conn(&E2EE_DB, |conn| {
conn.execute(
r#"
DELETE FROM pending_chat_secret_forwards
WHERE recipient_user_id = ?1 AND chat_id = ?2 AND secret_id = ?3
"#,
params![recipient_user_id, chat_id, secret_id],
)?;
Ok(())
})
}
/// Erase every E2EE record owned by, or queued for, a user. The operation is
/// intentionally idempotent so it can be retried after an interrupted remote /// intentionally idempotent so it can be retried after an interrupted remote
/// erasure request. /// erasure request.
pub fn purge_user(user_id: i64) -> Result<(), StorageError> { pub fn purge_user(user_id: i64) -> Result<(), StorageError> {
let user_id = user_id.to_string(); let user_id = user_id.to_string();
db::with_conn(&E2EE_DB, |conn| { db::with_conn(&E2EE_DB, |conn| {
let tx = conn.unchecked_transaction()?; let tx = conn.unchecked_transaction()?;
tx.execute("DELETE FROM chat_secrets WHERE user_id = ?1", params![user_id])?;
tx.execute( tx.execute(
"DELETE FROM pending_chat_secret_forwards WHERE recipient_user_id = ?1 OR sender_user_id = ?1", "DELETE FROM chat_secrets WHERE user_id = ?1",
params![user_id], params![user_id],
)?; )?;
tx.commit()?; tx.commit()?;
@ -242,17 +140,3 @@ fn chat_secret_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredChatS
updated_at: row.get(8)?, updated_at: row.get(8)?,
}) })
} }
fn pending_forward_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<PendingChatSecretForward> {
Ok(PendingChatSecretForward {
recipient_user_id: row.get(0)?,
chat_id: row.get(1)?,
sender_user_id: row.get(2)?,
secret_id: row.get(3)?,
version: row.get(4)?,
encrypted_secret: row.get(5)?,
kem_ciphertext: row.get(6)?,
wrapping_scheme: row.get(7)?,
created_at: row.get(8)?,
})
}

View file

@ -4,5 +4,7 @@ pub mod communities_util;
pub mod config_util; pub mod config_util;
pub mod db; pub mod db;
pub mod e2ee_storage; pub mod e2ee_storage;
pub mod relay_queue;
pub mod relay_replay;
pub mod settings; pub mod settings;
pub mod sync; pub mod sync;

View file

@ -0,0 +1,135 @@
use crate::storage_error::StorageError;
use crate::util::db;
use iota_util::route_target::RouteTarget;
use rusqlite::params;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingRelay {
pub id: i64,
pub target: RouteTarget,
pub frame: Vec<u8>,
pub created_at: i64,
pub frame_id: u32,
pub type_map_version: String,
}
pub fn enqueue(
target: RouteTarget,
frame: &[u8],
created_at: i64,
frame_id: u32,
type_map_version: &str,
) -> Result<(), StorageError> {
let destination_id = i64::try_from(target.id())
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
let target_kind = match target {
RouteTarget::User(_) => 0_i64,
RouteTarget::Iota(_) => 1_i64,
};
db::with_db(|connection| {
connection.execute(
"INSERT OR IGNORE INTO pending_relays (destination_id, target_kind, frame, created_at, frame_id, type_map_version) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
destination_id,
target_kind,
frame,
created_at,
i64::from(frame_id),
type_map_version
],
)?;
Ok(())
})
}
pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare(
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version FROM pending_relays ORDER BY id LIMIT ?1",
)?;
let rows = statement.query_map(params![limit.clamp(1, 500)], |row| {
let destination_id = row.get::<_, i64>(1)?;
let target_kind = row.get::<_, i64>(2)?;
let destination_id = u64::try_from(destination_id).map_err(|_| {
rusqlite::Error::FromSqlConversionFailure(
1,
rusqlite::types::Type::Integer,
"negative relay destination ID".into(),
)
})?;
let target = match target_kind {
0 => RouteTarget::User(destination_id),
1 => RouteTarget::Iota(destination_id),
_ => {
return Err(rusqlite::Error::FromSqlConversionFailure(
2,
rusqlite::types::Type::Integer,
"invalid relay target kind".into(),
));
}
};
Ok(PendingRelay {
id: row.get(0)?,
target,
frame: row.get(3)?,
created_at: row.get(4)?,
frame_id: u32::try_from(row.get::<_, i64>(5)?).map_err(|_| {
rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Integer,
"negative relay frame ID".into(),
)
})?,
type_map_version: row.get(6)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
})
}
pub fn acknowledge(destination_id: u64, frame_id: u32) -> Result<bool, StorageError> {
let destination_id = i64::try_from(destination_id)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_db(|connection| {
let changed = connection.execute(
"DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = 0 AND frame_id = ?2",
params![destination_id, i64::from(frame_id)],
)?;
Ok(changed == 1)
})
}
pub fn acknowledge_iota(destination_id: u64, frame_id: u32) -> Result<bool, StorageError> {
let destination_id = i64::try_from(destination_id)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_db(|connection| {
let changed = connection.execute(
"DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2",
params![destination_id, i64::from(frame_id)],
)?;
Ok(changed == 1)
})
}
pub fn remove_for_frame(target: RouteTarget, frame_id: u32) -> Result<bool, StorageError> {
let destination_id = i64::try_from(target.id())
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
let target_kind = match target {
RouteTarget::User(_) => 0_i64,
RouteTarget::Iota(_) => 1_i64,
};
db::with_db(|connection| {
let changed = connection.execute(
"DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = ?2 AND frame_id = ?3",
params![destination_id, target_kind, i64::from(frame_id)],
)?;
Ok(changed == 1)
})
}
pub fn delete(id: i64) -> Result<(), StorageError> {
db::with_db(|connection| {
connection.execute("DELETE FROM pending_relays WHERE id = ?1", params![id])?;
Ok(())
})
}

View file

@ -0,0 +1,152 @@
use crate::storage_error::StorageError;
use crate::util::db;
use rusqlite::params;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelayReservation {
New,
Existing { state: String, frame_matches: bool },
}
pub fn reserve(
signer_id: u64,
message_id: &str,
created_at: u64,
destination_id: u64,
frame: &[u8],
frame_id: u32,
type_map_version: &str,
) -> Result<RelayReservation, StorageError> {
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let created_at = i64::try_from(created_at)
.map_err(|_| StorageError::Other("relay creation time exceeds SQLite range".into()))?;
let destination_id = i64::try_from(destination_id)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_db(|connection| {
let inserted = connection.execute(
"INSERT OR IGNORE INTO relay_inbox (signer_id, message_id, created_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'received')",
params![
signer_id,
message_id,
created_at,
destination_id,
frame,
i64::from(frame_id),
type_map_version
],
)?;
if inserted == 1 {
return Ok(RelayReservation::New);
}
let (state, existing_destination_id, existing_frame, existing_type_map_version):
(String, i64, Vec<u8>, String) = connection.query_row(
"SELECT state, destination_id, frame, type_map_version FROM relay_inbox WHERE signer_id = ?1 AND message_id = ?2",
params![signer_id, message_id],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get::<_, Vec<u8>>(2)?,
row.get(3)?,
))
},
)?;
Ok(RelayReservation::Existing {
state,
frame_matches: existing_destination_id == destination_id
&& existing_frame == frame
&& existing_type_map_version == type_map_version,
})
})
}
pub fn mark_delivered_for_frame(destination_id: u64, frame_id: u32) -> Result<(), StorageError> {
let destination_id = i64::try_from(destination_id)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_db(|connection| {
connection.execute(
"UPDATE relay_inbox SET state = 'delivered' WHERE destination_id = ?1 AND frame_id = ?2",
params![destination_id, i64::from(frame_id)],
)?;
Ok(())
})
}
pub fn mark_state(signer_id: u64, message_id: &str, state: &str) -> Result<(), StorageError> {
if !matches!(
state,
"received" | "applied" | "queued" | "delivered" | "rejected"
) {
return Err(StorageError::Other("invalid relay inbox state".into()));
}
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
db::with_db(|connection| {
connection.execute(
"UPDATE relay_inbox SET state = ?3 WHERE signer_id = ?1 AND message_id = ?2",
params![signer_id, message_id, state],
)?;
Ok(())
})
}
pub fn prune_completed(before_created_at: i64) -> Result<(), StorageError> {
db::with_db(|connection| {
connection.execute(
"DELETE FROM relay_inbox WHERE created_at < ?1 AND state IN ('delivered', 'rejected')",
params![before_created_at],
)?;
connection.execute(
"DELETE FROM relay_replay WHERE created_at < ?1",
params![before_created_at],
)?;
Ok(())
})
}
pub fn accept(signer_id: u64, message_id: &str, created_at: u64) -> Result<bool, StorageError> {
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let created_at = i64::try_from(created_at)
.map_err(|_| StorageError::Other("relay creation time exceeds SQLite range".into()))?;
db::with_db(|connection| {
let inserted = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![signer_id, message_id, created_at],
)?;
Ok(inserted == 1)
})
}
#[cfg(test)]
mod tests {
use rusqlite::{Connection, params};
#[test]
fn replay_identity_uses_signer_and_message_id() -> Result<(), rusqlite::Error> {
let connection = Connection::open_in_memory()?;
connection.execute_batch(
"CREATE TABLE relay_replay (signer_id INTEGER NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (signer_id, message_id));",
)?;
let first = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![7_i64, "message", 1_i64],
)?;
let duplicate = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![7_i64, "message", 2_i64],
)?;
let other_signer = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![8_i64, "message", 2_i64],
)?;
assert_eq!((first, duplicate, other_signer), (1, 0, 1));
Ok(())
}
}

View file

@ -5,7 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
iota-paths = { path = "../iota-paths" } iota-paths = { path = "../iota-paths" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
"crypto" "crypto"
] } ] }

View file

@ -1,5 +1,50 @@
use base64::{Engine as _, engine::general_purpose::STANDARD}; use base64::{Engine as _, engine::general_purpose::STANDARD};
use mtp::crypto::{EncryptionType, Keyring, PublicKeyBundle, decrypt_with, encrypt_for}; use mtp::crypto::{
EncryptionType, Keyring, MultiEncryptedMessage, PublicKeyBundle, decrypt_multi_for,
encrypt_multi_for,
};
const CHALLENGE_PURPOSE: u8 = 0x01;
const LEGACY_AAD_DOMAIN: &[u8] = b"IOTA-MTP-AAD-1";
fn bind_aad(plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, String> {
let aad_len = u32::try_from(aad.len())
.map_err(|_| "associated data is too large to encode".to_string())?;
let mut bound = Vec::with_capacity(
LEGACY_AAD_DOMAIN
.len()
.saturating_add(4)
.saturating_add(aad.len())
.saturating_add(plaintext.len()),
);
bound.extend_from_slice(LEGACY_AAD_DOMAIN);
bound.extend_from_slice(&aad_len.to_be_bytes());
bound.extend_from_slice(aad);
bound.extend_from_slice(plaintext);
Ok(bound)
}
fn unbind_aad(bound: &[u8], aad: &[u8]) -> Result<Vec<u8>, String> {
let header_len = LEGACY_AAD_DOMAIN.len() + 4;
if bound.len() < header_len || &bound[..LEGACY_AAD_DOMAIN.len()] != LEGACY_AAD_DOMAIN {
return Err("associated-data binding is invalid".to_string());
}
let length_start = LEGACY_AAD_DOMAIN.len();
let length_end = length_start + 4;
let aad_len = u32::from_be_bytes(
bound[length_start..length_end]
.try_into()
.map_err(|_| "associated-data length is invalid".to_string())?,
) as usize;
let aad_start = length_end;
let aad_end = aad_start
.checked_add(aad_len)
.ok_or_else(|| "associated-data length overflows".to_string())?;
if aad_end > bound.len() || &bound[aad_start..aad_end] != aad {
return Err("associated data does not match".to_string());
}
Ok(bound[aad_end..].to_vec())
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum DataFormat { pub enum DataFormat {
@ -13,17 +58,25 @@ pub fn encrypt(
aad: &[u8], aad: &[u8],
recipient_pub_key_bundle: &PublicKeyBundle, recipient_pub_key_bundle: &PublicKeyBundle,
) -> Result<Vec<u8>, String> { ) -> Result<Vec<u8>, String> {
encrypt_for( let bound_plaintext = bind_aad(plaintext, aad)?;
let encrypted = encrypt_multi_for(
EncryptionType::MlKemChaCha20Poly1305, EncryptionType::MlKemChaCha20Poly1305,
recipient_pub_key_bundle, CHALLENGE_PURPOSE,
plaintext, &bound_plaintext,
aad, std::slice::from_ref(recipient_pub_key_bundle),
) )
.map_err(|e| format!("encryption error: {:?}", e)) .map_err(|e| format!("encryption error: {e:?}"))?;
encrypted
.to_bytes()
.map_err(|e| format!("encryption encoding error: {e:?}"))
} }
pub fn decrypt(ciphertext: &[u8], aad: &[u8], keyring: &Keyring) -> Result<Vec<u8>, String> { pub fn decrypt(ciphertext: &[u8], aad: &[u8], keyring: &Keyring) -> Result<Vec<u8>, String> {
decrypt_with(ciphertext, keyring, aad).map_err(|e| format!("decryption error: {:?}", e)) let message = MultiEncryptedMessage::from_bytes(ciphertext)
.map_err(|e| format!("decryption envelope error: {e:?}"))?;
let bound_plaintext = decrypt_multi_for(&message, CHALLENGE_PURPOSE, keyring)
.map_err(|e| format!("decryption error: {e:?}"))?;
unbind_aad(&bound_plaintext, aad)
} }
pub fn encrypt_challenge( pub fn encrypt_challenge(
@ -51,3 +104,18 @@ pub fn export(data: &[u8], format: DataFormat) -> Result<String, String> {
DataFormat::Hex => Ok(hex::encode(data)), DataFormat::Hex => Ok(hex::encode(data)),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encrypt_decrypt_binds_associated_data() -> Result<(), String> {
let keyring = Keyring::generate();
let ciphertext = encrypt(b"challenge", b"context", &keyring.public_key_bundle())?;
assert_eq!(decrypt(&ciphertext, b"context", &keyring)?, b"challenge");
assert!(decrypt(&ciphertext, b"other-context", &keyring).is_err());
Ok(())
}
}

View file

@ -44,7 +44,9 @@ pub fn delete_user_directory(user_id: i64) -> io::Result<()> {
} }
pub fn credential_path(user_id: i64) -> PathBuf { pub fn credential_path(user_id: i64) -> PathBuf {
storage_directory().join("credentials").join(format!("{user_id}.tu")) storage_directory()
.join("credentials")
.join(format!("{user_id}.tu"))
} }
pub fn read_user_credential(user_id: i64) -> io::Result<Option<String>> { pub fn read_user_credential(user_id: i64) -> io::Result<Option<String>> {
@ -58,7 +60,10 @@ pub fn read_user_credential(user_id: i64) -> io::Result<Option<String>> {
/// Resolve a credential by immutable account id. A valid legacy /// Resolve a credential by immutable account id. A valid legacy
/// `<username>.tu` is migrated atomically the first time it is encountered. /// `<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>> { 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)? { if let Some(credential) = read_user_credential(user_id)? {
return Ok(Some(credential)); return Ok(Some(credential));
} }
@ -71,7 +76,10 @@ pub fn read_user_credential_with_legacy(user_id: i64, username: &str) -> io::Res
let parsed = crate::tu::TuCredential::parse(&credential) let parsed = crate::tu::TuCredential::parse(&credential)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if parsed.user_id != user_id { if parsed.user_id != user_id {
return Err(io::Error::new(io::ErrorKind::InvalidData, "legacy credential user id mismatch")); return Err(io::Error::new(
io::ErrorKind::InvalidData,
"legacy credential user id mismatch",
));
} }
write_user_credential(user_id, &parsed.to_canonical_string())?; write_user_credential(user_id, &parsed.to_canonical_string())?;
fs::remove_file(legacy)?; fs::remove_file(legacy)?;

View file

@ -1,4 +1,6 @@
pub mod crypto_helper; pub mod crypto_helper;
pub mod crypto_util; pub mod crypto_util;
pub mod file_util; pub mod file_util;
pub mod mtp_compat;
pub mod route_target;
pub mod tu; pub mod tu;

View file

@ -0,0 +1,67 @@
use mtp::codec::{CommunicationValue, DataValue};
use mtp::type_map::DataTypeId;
/*
* Keep legacy control-plane handlers source-compatible while they migrate to
* MTP's explicit optional routing fields. Relay handlers must use sender() and
* receiver() directly so an absent outer sender cannot become an identity.
*/
pub trait CommunicationValueCompat {
fn get_id(&self) -> u32;
fn get_sender(&self) -> u64;
fn get_receiver(&self) -> u64;
}
impl CommunicationValueCompat for CommunicationValue {
fn get_id(&self) -> u32 {
self.id().unwrap_or_default()
}
fn get_sender(&self) -> u64 {
self.sender().unwrap_or_default()
}
fn get_receiver(&self) -> u64 {
self.receiver().unwrap_or_default()
}
}
pub trait OptionalDataValueExt<'a> {
fn as_bool(self) -> Option<bool>;
fn as_str(self) -> Option<&'a str>;
fn as_string(self) -> Option<String>;
fn as_number(self) -> Option<i128>;
fn as_signed_number(self) -> Option<i128>;
fn as_array(self) -> Option<Vec<DataValue>>;
fn as_container(self) -> Option<Vec<(DataTypeId, DataValue)>>;
}
impl<'a> OptionalDataValueExt<'a> for Option<&'a DataValue> {
fn as_bool(self) -> Option<bool> {
self.and_then(DataValue::as_bool)
}
fn as_str(self) -> Option<&'a str> {
self.and_then(DataValue::as_str)
}
fn as_string(self) -> Option<String> {
self.and_then(DataValue::as_string)
}
fn as_number(self) -> Option<i128> {
self.and_then(DataValue::as_number)
}
fn as_signed_number(self) -> Option<i128> {
self.and_then(DataValue::as_signed_number)
}
fn as_array(self) -> Option<Vec<DataValue>> {
self.and_then(DataValue::as_array)
}
fn as_container(self) -> Option<Vec<(DataTypeId, DataValue)>> {
self.and_then(DataValue::as_container)
}
}

View file

@ -0,0 +1,44 @@
const TARGET_KIND_MASK: u64 = 0xC000_0000_0000_0000;
const TARGET_ID_MASK: u64 = (1_u64 << 48) - 1;
const USER_TARGET_KIND: u64 = 0x4000_0000_0000_0000;
const IOTA_TARGET_KIND: u64 = 0x8000_0000_0000_0000;
/*
* Relay receivers carry their namespace in the wire identity. This prevents
* a user ID and an Iota ID with the same numeric value from selecting the
* wrong connection at an Omikron.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteTarget {
User(u64),
Iota(u64),
}
impl RouteTarget {
pub fn wire_id(self) -> Option<u64> {
let (kind, id) = match self {
Self::User(id) => (USER_TARGET_KIND, id),
Self::Iota(id) => (IOTA_TARGET_KIND, id),
};
(id > 0 && id <= TARGET_ID_MASK).then_some(kind | id)
}
pub fn from_wire_id(value: u64) -> Option<Self> {
let id = value & TARGET_ID_MASK;
if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 {
return None;
}
match value & TARGET_KIND_MASK {
USER_TARGET_KIND => Some(Self::User(id)),
IOTA_TARGET_KIND => Some(Self::Iota(id)),
_ => None,
}
}
pub const fn id(self) -> u64 {
match self {
Self::User(id) | Self::Iota(id) => id,
}
}
}

View file

@ -47,7 +47,10 @@ impl fmt::Debug for TuCredential {
impl TuCredential { impl TuCredential {
pub fn parse(input: &str) -> Result<Self, TuError> { pub fn parse(input: &str) -> Result<Self, TuError> {
let (identity, encoded_keyring) = input.trim().split_once("::").ok_or(TuError::InvalidFormat)?; let (identity, encoded_keyring) = input
.trim()
.split_once("::")
.ok_or(TuError::InvalidFormat)?;
if encoded_keyring.is_empty() || encoded_keyring.contains("::") { if encoded_keyring.is_empty() || encoded_keyring.contains("::") {
return Err(TuError::InvalidFormat); return Err(TuError::InvalidFormat);
} }
@ -60,7 +63,11 @@ impl TuCredential {
return Err(TuError::InvalidUserId); return Err(TuError::InvalidUserId);
} }
let keyring = keyring_from_base64(encoded_keyring).ok_or(TuError::InvalidKeyring)?; let keyring = keyring_from_base64(encoded_keyring).ok_or(TuError::InvalidKeyring)?;
Ok(Self { user_id, omega_host: omega_host.trim().to_owned(), keyring }) Ok(Self {
user_id,
omega_host: omega_host.trim().to_owned(),
keyring,
})
} }
pub fn public_key_bundle(&self) -> PublicKeyBundle { pub fn public_key_bundle(&self) -> PublicKeyBundle {
@ -68,7 +75,12 @@ impl TuCredential {
} }
pub fn to_canonical_string(&self) -> String { pub fn to_canonical_string(&self) -> String {
format!("{}@{}::{}", self.user_id, self.omega_host, keyring_to_base64(&self.keyring)) format!(
"{}@{}::{}",
self.user_id,
self.omega_host,
keyring_to_base64(&self.keyring)
)
} }
} }
@ -79,16 +91,31 @@ mod tests {
#[test] #[test]
fn round_trip_is_canonical() { fn round_trip_is_canonical() {
let credential = TuCredential { user_id: 42, omega_host: "omega.example:443".into(), keyring: generate_keyring() }; let credential = TuCredential {
user_id: 42,
omega_host: "omega.example:443".into(),
keyring: generate_keyring(),
};
let parsed = TuCredential::parse(&credential.to_canonical_string()).unwrap(); let parsed = TuCredential::parse(&credential.to_canonical_string()).unwrap();
assert_eq!(parsed.user_id, 42); assert_eq!(parsed.user_id, 42);
assert_eq!(parsed.omega_host, "omega.example:443"); assert_eq!(parsed.omega_host, "omega.example:443");
assert_eq!(parsed.to_canonical_string(), credential.to_canonical_string()); assert_eq!(
parsed.to_canonical_string(),
credential.to_canonical_string()
);
} }
#[test] #[test]
fn rejects_malformed_credentials() { fn rejects_malformed_credentials() {
for value in ["", "1@omega", "@omega::abc", "0@omega::abc", "281474976710656@omega::abc", "1@::abc", "1@omega::abc::def"] { for value in [
"",
"1@omega",
"@omega::abc",
"0@omega::abc",
"281474976710656@omega::abc",
"1@::abc",
"1@omega::abc::def",
] {
assert!(TuCredential::parse(value).is_err(), "{value}"); assert!(TuCredential::parse(value).is_err(), "{value}");
} }
} }

View file

@ -13,10 +13,8 @@ impl Default for ColorConfig {
impl ColorConfig { impl ColorConfig {
pub fn new() -> Self { pub fn new() -> Self {
let enabled = env::var("NO_COLOR").is_err() let enabled =
&& env::var("TERM") env::var("NO_COLOR").is_err() && env::var("TERM").map(|t| t != "dumb").unwrap_or(true);
.map(|t| t != "dumb")
.unwrap_or(true);
Self { enabled } Self { enabled }
} }

View file

@ -386,17 +386,11 @@ fn writable_socket_path(path: &Path) -> Result<(), StartupError> {
fn print_help() { fn print_help() {
let color = cli_color::ColorConfig::new(); let color = cli_color::ColorConfig::new();
println!( println!("{}", cli_color::heading(&color, "Iota Operator Console"));
"{}",
cli_color::heading(&color, "Iota Operator Console")
);
println!(); println!();
println!("Usage: iota [OPTIONS] [COMMAND]"); println!("Usage: iota [OPTIONS] [COMMAND]");
println!(); println!();
println!( println!("{}", cli_color::info(&color, "Commands:"));
"{}",
cli_color::info(&color, "Commands:")
);
println!(" (no command) Launch the interactive dashboard"); println!(" (no command) Launch the interactive dashboard");
println!(" status Show daemon status"); println!(" status Show daemon status");
println!(" tasks List active tasks"); println!(" tasks List active tasks");
@ -432,10 +426,7 @@ fn print_help() {
println!(" completions <SHELL> Generate shell completions"); println!(" completions <SHELL> Generate shell completions");
println!(" man Show the man page"); println!(" man Show the man page");
println!(); println!();
println!( println!("{}", cli_color::info(&color, "Options:"));
"{}",
cli_color::info(&color, "Options:")
);
println!(" --theme <THEME> Theme: monospace, binary, ansi, surface"); println!(" --theme <THEME> Theme: monospace, binary, ansi, surface");
println!(" --output <FORMAT> Output format: text, json, yaml, table"); println!(" --output <FORMAT> Output format: text, json, yaml, table");
println!(" --color <WHEN> Color: auto, always, never"); println!(" --color <WHEN> Color: auto, always, never");
@ -445,10 +436,7 @@ fn print_help() {
println!(" -h, --help Show help"); println!(" -h, --help Show help");
println!(" -V, --version Show version"); println!(" -V, --version Show version");
println!(); println!();
println!( println!("{}", cli_color::info(&color, "Examples:"));
"{}",
cli_color::info(&color, "Examples:")
);
println!(" iota Launch the interactive dashboard"); println!(" iota Launch the interactive dashboard");
println!(" iota status Show daemon status"); println!(" iota status Show daemon status");
println!(" iota users list --output=json List users in JSON format"); println!(" iota users list --output=json List users in JSON format");
@ -458,19 +446,13 @@ fn print_help() {
println!(" iota logs --limit 50 Show last 50 log entries"); println!(" iota logs --limit 50 Show last 50 log entries");
println!(" iota completions bash Generate bash completions"); println!(" iota completions bash Generate bash completions");
println!(); println!();
println!( println!("{}", cli_color::info(&color, "Exit Codes:"));
"{}",
cli_color::info(&color, "Exit Codes:")
);
println!(" 0 Success"); println!(" 0 Success");
println!(" 1 General error"); println!(" 1 General error");
println!(" 2 Invalid command or arguments"); println!(" 2 Invalid command or arguments");
println!(" 130 Interrupted (Ctrl+C)"); println!(" 130 Interrupted (Ctrl+C)");
println!(); println!();
println!( println!("{}", cli_color::muted(&color, "Environment Variables:"));
"{}",
cli_color::muted(&color, "Environment Variables:")
);
println!(" NO_COLOR Disable colored output when set"); println!(" NO_COLOR Disable colored output when set");
println!(" TERM Terminal type (dumb disables colors)"); println!(" TERM Terminal type (dumb disables colors)");
println!(" IOTA_THEME Default theme override"); println!(" IOTA_THEME Default theme override");
@ -566,13 +548,26 @@ async fn run_command(
Command::Tasks => LocalRequest::ListTasks, Command::Tasks => LocalRequest::ListTasks,
Command::UsersList => LocalRequest::ListUsers, Command::UsersList => LocalRequest::ListUsers,
Command::UsersShow { user_id } => LocalRequest::GetUser { user_id }, Command::UsersShow { user_id } => LocalRequest::GetUser { user_id },
Command::UsersAdd { username: Some(username), tu: None } => LocalRequest::CreateUser { username }, Command::UsersAdd {
Command::UsersAdd { username: None, tu: Some(path) } => { username: Some(username),
let contents = std::fs::read_to_string(&path) tu: None,
.map_err(|error| StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())))?; } => LocalRequest::CreateUser { username },
iota_util::tu::TuCredential::parse(&contents) Command::UsersAdd {
.map_err(|error| StartupError::InvalidCommand(format!("Invalid credential {}: {error}", path.display())))?; username: None,
LocalRequest::AttachUserFromTu { credential: iota_ipc::SecretString(contents) } 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 { .. } => { Command::UsersAdd { .. } => {
return Err(StartupError::InvalidCommand( return Err(StartupError::InvalidCommand(
@ -583,22 +578,43 @@ async fn run_command(
user_id, user_id,
confirmed: true, confirmed: true,
} => LocalRequest::ReleaseUser { user_id }, } => LocalRequest::ReleaseUser { user_id },
Command::UsersPurgeData { user_id, confirmed: true } => LocalRequest::PurgeUserData { user_id }, Command::UsersPurgeData {
Command::UsersCompleteDelete { user_id, tu, confirmed: true } => { user_id,
confirmed: true,
} => LocalRequest::PurgeUserData { user_id },
Command::UsersCompleteDelete {
user_id,
tu,
confirmed: true,
} => {
let credential = match tu { let credential = match tu {
Some(path) => { Some(path) => {
let contents = std::fs::read_to_string(&path) let contents = std::fs::read_to_string(&path).map_err(|error| {
.map_err(|error| StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())))?; StartupError::InvalidCommand(format!(
let parsed = iota_util::tu::TuCredential::parse(&contents) "Cannot read {}: {error}",
.map_err(|error| StartupError::InvalidCommand(format!("Invalid credential {}: {error}", path.display())))?; 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 { if parsed.user_id != user_id {
return Err(StartupError::InvalidCommand("credential user ID does not match complete-delete target".into())); return Err(StartupError::InvalidCommand(
"credential user ID does not match complete-delete target".into(),
));
} }
Some(iota_ipc::SecretString(contents)) Some(iota_ipc::SecretString(contents))
} }
None => None, None => None,
}; };
LocalRequest::CompleteDeleteUser { user_id, credential } LocalRequest::CompleteDeleteUser {
user_id,
credential,
}
} }
Command::OmikronReconnect => LocalRequest::ReconnectOmikron, Command::OmikronReconnect => LocalRequest::ReconnectOmikron,
Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity, Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity,
@ -621,8 +637,12 @@ async fn run_command(
Command::UsersRelease { Command::UsersRelease {
confirmed: false, .. confirmed: false, ..
} }
| Command::UsersPurgeData { confirmed: false, .. } | Command::UsersPurgeData {
| Command::UsersCompleteDelete { confirmed: false, .. } confirmed: false, ..
}
| Command::UsersCompleteDelete {
confirmed: false, ..
}
| Command::IdentityRotate { confirmed: false } | Command::IdentityRotate { confirmed: false }
| Command::RegenerateKeys { confirmed: false } | Command::RegenerateKeys { confirmed: false }
| Command::DaemonRestart { confirmed: false } | Command::DaemonRestart { confirmed: false }
@ -715,14 +735,14 @@ async fn run_command(
); );
} }
ResponsePayload::UserRemoved { user_id } => { ResponsePayload::UserRemoved { user_id } => {
println!( println!("{} {}", cli_color::warning(&color, "Removed user"), user_id);
"{} {}",
cli_color::warning(&color, "Removed user"),
user_id
);
} }
ResponsePayload::UserDataPurged { user_id } => { ResponsePayload::UserDataPurged { user_id } => {
println!("{} hosted data for {}. Account remains managed by this Iota.", cli_color::success(&color, "Purged"), user_id); println!(
"{} hosted data for {}. Account remains managed by this Iota.",
cli_color::success(&color, "Purged"),
user_id
);
} }
ResponsePayload::Acknowledged { message } => { ResponsePayload::Acknowledged { message } => {
println!("{}", message); println!("{}", message);
@ -740,11 +760,7 @@ async fn run_command(
status.connected status.connected
); );
if let Some(id) = status.iota_id { if let Some(id) = status.iota_id {
println!( println!("{}: {}", cli_color::info(&color, "Iota ID"), id);
"{}: {}",
cli_color::info(&color, "Iota ID"),
id
);
} }
} }
ResponsePayload::Components(components) => { ResponsePayload::Components(components) => {
@ -756,9 +772,7 @@ async fn run_command(
} else { } else {
for comp in &components { for comp in &components {
let (status_str, style) = match comp.status { let (status_str, style) = match comp.status {
iota_ipc::HealthStatus::Healthy => { iota_ipc::HealthStatus::Healthy => ("healthy", cli_color::SUCCESS),
("healthy", cli_color::SUCCESS)
}
iota_ipc::HealthStatus::Degraded => { iota_ipc::HealthStatus::Degraded => {
("degraded", cli_color::WARNING) ("degraded", cli_color::WARNING)
} }
@ -811,15 +825,9 @@ async fn run_command(
} }
ResponsePayload::UpdateStatus(status) => { ResponsePayload::UpdateStatus(status) => {
if status.available { if status.available {
println!( println!("{}", cli_color::success(&color, "Update available."));
"{}",
cli_color::success(&color, "Update available.")
);
} else { } else {
println!( println!("{}", cli_color::info(&color, "Up to date."));
"{}",
cli_color::info(&color, "Up to date.")
);
} }
} }
ResponsePayload::Communities(communities) => { ResponsePayload::Communities(communities) => {
@ -827,11 +835,7 @@ async fn run_command(
println!("{}", cli_color::muted(&color, "No communities.")); println!("{}", cli_color::muted(&color, "No communities."));
} else { } else {
for c in &communities { for c in &communities {
println!( println!("{} ({})", cli_color::heading(&color, &c.title), c.name);
"{} ({})",
cli_color::heading(&color, &c.title),
c.name
);
} }
} }
} }
@ -905,7 +909,12 @@ fn render_table(payload: &ResponsePayload) {
iota_ipc::HealthStatus::Failed => "failed", iota_ipc::HealthStatus::Failed => "failed",
}; };
let message = comp.message.as_deref().unwrap_or("-"); let message = comp.message.as_deref().unwrap_or("-");
println!("{:<20} {:<10} {}", format!("{:?}", comp.id), status_str, message); println!(
"{:<20} {:<10} {}",
format!("{:?}", comp.id),
status_str,
message
);
} }
} }
ResponsePayload::Communities(communities) => { ResponsePayload::Communities(communities) => {
@ -924,8 +933,14 @@ fn render_table(payload: &ResponsePayload) {
println!("No log entries."); println!("No log entries.");
return; return;
} }
println!("{:<20} {:<6} {:<12} {}", "TIMESTAMP", "LEVEL", "SENDER", "MESSAGE"); println!(
println!("{:<20} {:<6} {:<12} {}", "--------", "--------", "--------", "--------"); "{:<20} {:<6} {:<12} {}",
"TIMESTAMP", "LEVEL", "SENDER", "MESSAGE"
);
println!(
"{:<20} {:<6} {:<12} {}",
"--------", "--------", "--------", "--------"
);
for entry in &logs.entries { for entry in &logs.entries {
let level = if entry.is_error { "ERR" } else { "INF" }; let level = if entry.is_error { "ERR" } else { "INF" };
println!( println!(

View file

@ -43,34 +43,24 @@ impl StartupError {
Self::SystemPermissionDenied(_) => { Self::SystemPermissionDenied(_) => {
Some("Run with appropriate privileges or use a user-level daemon instead.") Some("Run with appropriate privileges or use a user-level daemon instead.")
} }
Self::SocketPermissionDenied(_) => { Self::SocketPermissionDenied(_) => Some(
Some( "Check file permissions on the socket or ensure the daemon is running as your user.",
"Check file permissions on the socket or ensure the daemon is running as your user.", ),
) Self::IpcTimedOut(_) => Some(
} "The daemon may be starting up. Wait a moment and try again, or check daemon logs.",
Self::IpcTimedOut(_) => { ),
Some( Self::ProtocolMismatch { .. } => Some("Update your CLI or daemon to match versions."),
"The daemon may be starting up. Wait a moment and try again, or check daemon logs.", Self::DaemonExited { .. } => Some("Restart the daemon with `iota daemon restart`."),
) Self::IpcBindUnavailable(_) => Some(
} "Another instance may be running. Stop it first or use a different socket path.",
Self::ProtocolMismatch { .. } => { ),
Some("Update your CLI or daemon to match versions.")
}
Self::DaemonExited { .. } => {
Some("Restart the daemon with `iota daemon restart`.")
}
Self::IpcBindUnavailable(_) => {
Some("Another instance may be running. Stop it first or use a different socket path.")
}
Self::Terminal(_) => { Self::Terminal(_) => {
Some("Use a terminal that supports interactive mode, or run commands headlessly.") Some("Use a terminal that supports interactive mode, or run commands headlessly.")
} }
Self::Consent(_) => { Self::Consent(_) => Some(
Some("Run `iota terms accept` in an interactive terminal to review and accept terms.") "Run `iota terms accept` in an interactive terminal to review and accept terms.",
} ),
Self::InvalidCommand(_) => { Self::InvalidCommand(_) => Some("Run `iota --help` to see available commands."),
Some("Run `iota --help` to see available commands.")
}
_ => None, _ => None,
} }
} }
@ -149,8 +139,16 @@ mod tests {
} }
#[test] #[test]
fn most_errors_have_suggestions() { fn most_errors_have_suggestions() {
assert!(StartupError::DaemonExecutableMissing(PathBuf::from("iota-daemon")).suggestion().is_some()); assert!(
assert!(StartupError::IpcTimedOut(PathBuf::from("/tmp/iota.sock")).suggestion().is_some()); StartupError::DaemonExecutableMissing(PathBuf::from("iota-daemon"))
.suggestion()
.is_some()
);
assert!(
StartupError::IpcTimedOut(PathBuf::from("/tmp/iota.sock"))
.suggestion()
.is_some()
);
assert!(StartupError::Cancelled.suggestion().is_none()); assert!(StartupError::Cancelled.suggestion().is_none());
} }
} }

View file

@ -14,6 +14,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
"client", "client",
"crypto", "crypto",
"files", "files",
"raw",
] } ] }
dashmap = "6.2.1" dashmap = "6.2.1"

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ use iota_storage::users::user_profile::UserProfile;
use iota_storage::util::config_util::CONFIG; use iota_storage::util::config_util::CONFIG;
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64}; use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
use iota_util::file_util::write_user_credential; use iota_util::file_util::write_user_credential;
use iota_util::mtp_compat::OptionalDataValueExt;
use iota_util::tu::TuCredential; use iota_util::tu::TuCredential;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme}; use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme};
@ -33,7 +34,9 @@ pub enum LifecycleUserError {
} }
impl From<crate::OmikronError> for LifecycleUserError { impl From<crate::OmikronError> for LifecycleUserError {
fn from(value: crate::OmikronError) -> Self { Self::Transport(value) } fn from(value: crate::OmikronError) -> Self {
Self::Transport(value)
}
} }
fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec<u8> { fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec<u8> {
@ -46,21 +49,31 @@ fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> V
} }
fn configured_iota_id() -> Result<i64, LifecycleUserError> { fn configured_iota_id() -> Result<i64, LifecycleUserError> {
CONFIG.load().iota_id CONFIG
.load()
.iota_id
.and_then(|id| i64::try_from(id).ok()) .and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0) .filter(|id| *id > 0)
.ok_or_else(|| LifecycleUserError::InvalidCredential("Iota identity is not registered".into())) .ok_or_else(|| {
LifecycleUserError::InvalidCredential("Iota identity is not registered".into())
})
} }
fn sign_lifecycle_payload(credential: &TuCredential, payload: &[u8]) -> Result<(Vec<u8>, Vec<u8>), LifecycleUserError> { fn sign_lifecycle_payload(
credential: &TuCredential,
payload: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), LifecycleUserError> {
let classical = Ed25519Signer::new(&credential.keyring.sig_cl_secret_key) let classical = Ed25519Signer::new(&credential.keyring.sig_cl_secret_key)
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))? .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?
.sign(payload) .sign(payload)
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
let pq = MlDsaSigner::new(&credential.keyring.sig_pq_secret_key, &credential.keyring.sig_pq_public_key) let pq = MlDsaSigner::new(
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))? &credential.keyring.sig_pq_secret_key,
.sign(payload) &credential.keyring.sig_pq_public_key,
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; )
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?
.sign(payload)
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
Ok((classical, pq)) Ok((classical, pq))
} }
@ -71,15 +84,25 @@ async fn inspect_credential_account(
if credential.omega_host != omega_discovery::omega_host() { if credential.omega_host != omega_discovery::omega_host() {
return Err(LifecycleUserError::OmegaHostMismatch); return Err(LifecycleUserError::OmegaHostMismatch);
} }
let request = CommunicationValue::new(CommunicationType::GetUserData) let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
.add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into())); DataType::UserId,
let response = connection.await_response(&request, Duration::from_secs(20)).await?; DataValue::SignedNumber(credential.user_id.into()),
);
let response = connection
.await_response(&request, Duration::from_secs(20))
.await?;
if !response.is_type(CommunicationType::GetUserData) { if !response.is_type(CommunicationType::GetUserData) {
return Err(LifecycleUserError::RemoteRejected); return Err(LifecycleUserError::RemoteRejected);
} }
let username = response.get_data(DataType::Username).as_str().map(str::to_owned) let username = response
.get_data(DataType::Username)
.as_str()
.map(str::to_owned)
.ok_or(LifecycleUserError::RemoteRejected)?; .ok_or(LifecycleUserError::RemoteRejected)?;
let public_key = response.get_data(DataType::PublicKey).as_str().map(str::to_owned) let public_key = response
.get_data(DataType::PublicKey)
.as_str()
.map(str::to_owned)
.ok_or(LifecycleUserError::RemoteRejected)?; .ok_or(LifecycleUserError::RemoteRejected)?;
if public_key != public_key_bundle_to_base64(&credential.public_key_bundle()) { if public_key != public_key_bundle_to_base64(&credential.public_key_bundle()) {
return Err(LifecycleUserError::RemoteRejected); return Err(LifecycleUserError::RemoteRejected);
@ -96,54 +119,121 @@ async fn credential_proof(
domain: &[u8], domain: &[u8],
) -> Result<(), LifecycleUserError> { ) -> Result<(), LifecycleUserError> {
let iota_id = configured_iota_id()?; let iota_id = configured_iota_id()?;
let begin_request = CommunicationValue::new(begin) let begin_request = CommunicationValue::new(begin).add_typed_default(
.add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into())); DataType::UserId,
let challenge_response = connection.await_response(&begin_request, Duration::from_secs(20)).await?; 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) { if !challenge_response.is_type(challenge) {
return Err(LifecycleUserError::RemoteRejected); return Err(LifecycleUserError::RemoteRejected);
} }
let nonce = challenge_response.get_data(DataType::ServerNonce).as_signed_number() let nonce = challenge_response
.get_data(DataType::ServerNonce)
.as_signed_number()
.and_then(|value| u64::try_from(value).ok()) .and_then(|value| u64::try_from(value).ok())
.ok_or(LifecycleUserError::RemoteRejected)?; .ok_or(LifecycleUserError::RemoteRejected)?;
let (signature, pq_signature) = sign_lifecycle_payload(credential, &lifecycle_payload(domain, credential.user_id, iota_id, nonce))?; let (signature, pq_signature) = sign_lifecycle_payload(
credential,
&lifecycle_payload(domain, credential.user_id, iota_id, nonce),
)?;
let complete_request = CommunicationValue::new(complete) let complete_request = CommunicationValue::new(complete)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into())) .add_typed_default(
DataType::UserId,
DataValue::SignedNumber(credential.user_id.into()),
)
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())) .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into()))
.add_typed_default(DataType::Signature, DataValue::Bytes(signature)) .add_typed_default(DataType::Signature, DataValue::Bytes(signature))
.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); .add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
let response = connection.await_response(&complete_request, Duration::from_secs(20)).await?; let response = connection
if response.is_type(CommunicationType::Success) { Ok(()) } else { Err(LifecycleUserError::RemoteRejected) } .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 /// Attach or migrate an existing account. Local state is written only after
/// Omega has accepted the credential proof and changed its assignment. /// 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> { pub async fn attach_user_from_tu(
let credential = TuCredential::parse(contents).map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; 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?; 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?; credential_proof(
let profile = UserProfile::new(credential.user_id, username, None, public_key, hex_hash(contents), String::new()); 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()) write_user_credential(profile.user_id, &credential.to_canonical_string())
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; .map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
try_add_user(profile.clone()).map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; try_add_user(profile.clone())
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
Ok(profile) Ok(profile)
} }
pub async fn complete_delete_user_with_tu(connection: &dyn OmikronClient, contents: &str, expected_user_id: i64) -> Result<(), LifecycleUserError> { pub async fn complete_delete_user_with_tu(
let credential = TuCredential::parse(contents).map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; connection: &dyn OmikronClient,
if credential.user_id != expected_user_id { return Err(LifecycleUserError::InvalidCredential("credential user ID does not match deletion target".into())); } 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?; inspect_credential_account(connection, &credential).await?;
credential_proof(connection, &credential, CommunicationType::DeleteUserCredentialBegin, CommunicationType::DeleteUserCredentialChallenge, CommunicationType::DeleteUserCredentialComplete, b"tensamin:user-delete:v1\0").await credential_proof(
connection,
&credential,
CommunicationType::DeleteUserCredentialBegin,
CommunicationType::DeleteUserCredentialChallenge,
CommunicationType::DeleteUserCredentialComplete,
b"tensamin:user-delete:v1\0",
)
.await
} }
/// Repair local management state after a release or migration committed in /// Repair local management state after a release or migration committed in
/// Omega but local cleanup was interrupted. Hosted data is retained. /// Omega but local cleanup was interrupted. Hosted data is retained.
pub async fn reconcile_managed_users(connection: &dyn OmikronClient) { pub async fn reconcile_managed_users(connection: &dyn OmikronClient) {
let Ok(local_iota_id) = configured_iota_id() else { return; }; let Ok(local_iota_id) = configured_iota_id() else {
return;
};
for user in iota_storage::users::user_manager::get_users() { for user in iota_storage::users::user_manager::get_users() {
let request = CommunicationValue::new(CommunicationType::GetUserData) let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user.user_id.into())); DataType::UserId,
let Ok(response) = connection.await_response(&request, Duration::from_secs(10)).await else { continue; }; DataValue::SignedNumber(user.user_id.into()),
let remote_iota_id = response.get_data(DataType::IotaId).as_signed_number().and_then(|value| i64::try_from(value).ok()); );
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) { if remote_iota_id != Some(local_iota_id) {
let _ = iota_storage::users::user_manager::release_user(user.user_id); let _ = iota_storage::users::user_manager::release_user(user.user_id);
} }
@ -268,7 +358,12 @@ pub async fn create_user(
log!("Created User"); log!("Created User");
write_user_credential( write_user_credential(
user_id, user_id,
&format!("{}@{}::{}", user_id, omega_discovery::omega_host(), keyring_b64), &format!(
"{}@{}::{}",
user_id,
omega_discovery::omega_host(),
keyring_b64
),
) )
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?; .map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
@ -282,6 +377,7 @@ mod tests {
use super::{CreateUserError, request_user_id, valid_username}; use super::{CreateUserError, request_user_id, valid_username};
use crate::{OmikronClient, OmikronError}; use crate::{OmikronClient, OmikronError};
use async_trait::async_trait; use async_trait::async_trait;
use iota_util::mtp_compat::CommunicationValueCompat;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::time::Duration; use std::time::Duration;

View file

@ -5,7 +5,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] }
bytes = "1" bytes = "1"
http = "1" http = "1"
iota-logger = { path = "../iota-logger" } iota-logger = { path = "../iota-logger" }