feat(tauth): rework TAuth
This commit is contained in:
parent
3f2ac18333
commit
3685babebf
31 changed files with 3418 additions and 502 deletions
140
Cargo.lock
generated
140
Cargo.lock
generated
|
|
@ -1304,6 +1304,18 @@ dependencies = [
|
||||||
"simdutf8",
|
"simdutf8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-as-inner"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -2018,6 +2030,16 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -2090,6 +2112,7 @@ dependencies = [
|
||||||
name = "iota"
|
name = "iota"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"clap",
|
"clap",
|
||||||
"iota-cli",
|
"iota-cli",
|
||||||
"iota-core",
|
"iota-core",
|
||||||
|
|
@ -2199,6 +2222,8 @@ name = "iota-daemon-lib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"hex",
|
||||||
"iota-auth",
|
"iota-auth",
|
||||||
"iota-connection",
|
"iota-connection",
|
||||||
"iota-identity",
|
"iota-identity",
|
||||||
|
|
@ -2212,12 +2237,14 @@ dependencies = [
|
||||||
"mtp",
|
"mtp",
|
||||||
"omikron-connector",
|
"omikron-connector",
|
||||||
"other-iota",
|
"other-iota",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2311,6 +2338,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"sha2 0.10.9",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -2355,14 +2383,29 @@ dependencies = [
|
||||||
"iota-paths",
|
"iota-paths",
|
||||||
"mtp",
|
"mtp",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"sha2 0.10.9",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipconfig"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||||
|
dependencies = [
|
||||||
|
"socket2",
|
||||||
|
"widestring",
|
||||||
|
"windows-registry",
|
||||||
|
"windows-result",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.2"
|
version = "2.12.2"
|
||||||
|
|
@ -2579,6 +2622,12 @@ dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -2638,6 +2687,15 @@ dependencies = [
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-cache"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
@ -3125,6 +3183,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"hex",
|
||||||
"iota-auth",
|
"iota-auth",
|
||||||
"iota-connection",
|
"iota-connection",
|
||||||
"iota-identity",
|
"iota-identity",
|
||||||
|
|
@ -3136,8 +3195,14 @@ dependencies = [
|
||||||
"mtp",
|
"mtp",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"trust-dns-resolver",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3948,6 +4013,12 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resolv-conf"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
|
|
@ -4921,6 +4992,52 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trust-dns-proto"
|
||||||
|
version = "0.23.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"cfg-if",
|
||||||
|
"data-encoding",
|
||||||
|
"enum-as-inner",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"idna 0.4.0",
|
||||||
|
"ipnet",
|
||||||
|
"once_cell",
|
||||||
|
"rand 0.8.8",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tinyvec",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trust-dns-resolver"
|
||||||
|
version = "0.23.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"ipconfig",
|
||||||
|
"lru-cache",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"rand 0.8.8",
|
||||||
|
"resolv-conf",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"trust-dns-proto",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|
@ -4939,12 +5056,27 @@ version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.13.3"
|
version = "1.13.3"
|
||||||
|
|
@ -5009,7 +5141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna 1.1.0",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
@ -5283,6 +5415,12 @@ dependencies = [
|
||||||
"wezterm-dynamic",
|
"wezterm-dynamic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
|
||||||
|
|
@ -157,59 +157,6 @@ impl ClientConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if cv.is_type(CommunicationType::SaveAppData) {
|
|
||||||
let sender_id = match cv.require_sender() {
|
|
||||||
Ok(sender_id) => sender_id,
|
|
||||||
Err(_) => {
|
|
||||||
self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let _app_data = cv
|
|
||||||
.get_data(DataType::AppData)
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let res = CommunicationValue::new(CommunicationType::SaveAppData)
|
|
||||||
.with_request_id(&cv)
|
|
||||||
.with_receiver(sender_id);
|
|
||||||
self.send_message(&res).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cv.is_type(CommunicationType::LoadAppData) {
|
|
||||||
let sender_id = match cv.require_sender() {
|
|
||||||
Ok(sender_id) => sender_id,
|
|
||||||
Err(_) => {
|
|
||||||
self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let app_data = String::new();
|
|
||||||
|
|
||||||
let res = CommunicationValue::new(CommunicationType::LoadAppData)
|
|
||||||
.with_request_id(&cv)
|
|
||||||
.with_receiver(sender_id)
|
|
||||||
.add_typed_default(DataType::AppData, DataValue::Str(app_data));
|
|
||||||
self.send_message(&res).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cv.is_type(CommunicationType::CreateApp) {
|
|
||||||
self.send_message(&message_handlers::handle_create_app(&cv))
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cv.is_type(CommunicationType::DeleteApp) {
|
|
||||||
self.send_message(&message_handlers::handle_delete_app(&cv))
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cv.is_type(CommunicationType::AccountStateRequest) {
|
if cv.is_type(CommunicationType::AccountStateRequest) {
|
||||||
// Account initialization is routed through Omikron.
|
// Account initialization is routed through Omikron.
|
||||||
self.send_message(
|
self.send_message(
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,21 @@ impl IpcClient {
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResponsePayload::TAuthApps(apps) => apps
|
||||||
|
.iter()
|
||||||
|
.map(|app| format!("{} {} {}", app.app_id, app.domain, app.connection_mode))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
ResponsePayload::TAuthApp(app) => format!(
|
||||||
|
"TAuth app {}: domain={}, owner={}, mode={}, endpoint={}",
|
||||||
|
app.app_id, app.domain, app.owner_user_id, app.connection_mode, app.endpoint_url
|
||||||
|
),
|
||||||
|
ResponsePayload::TAuthAppCreated { app, .. } => {
|
||||||
|
format!("Created TAuth app {} for {}", app.app_id, app.domain)
|
||||||
|
}
|
||||||
|
ResponsePayload::TAuthAppCredentialExport { .. } => {
|
||||||
|
"TAuth credential export payload withheld.".into()
|
||||||
|
}
|
||||||
ResponsePayload::InvitationCreated(invitation) => format!(
|
ResponsePayload::InvitationCreated(invitation) => format!(
|
||||||
"Created {:?} invitation {}\nToken: {}\nExpires: {}{}{}",
|
"Created {:?} invitation {}\nToken: {}\nExpires: {}{}{}",
|
||||||
invitation.authority,
|
invitation.authority,
|
||||||
|
|
@ -538,12 +553,12 @@ impl IpcClient {
|
||||||
format!("Reconciled user {}: {:?}", result.user_id, result.action)
|
format!("Reconciled user {}: {:?}", result.user_id, result.action)
|
||||||
}
|
}
|
||||||
ResponsePayload::UserDiagnostics(diagnostics) => format!(
|
ResponsePayload::UserDiagnostics(diagnostics) => format!(
|
||||||
"User {}: state={:?}, data={}, credential={:?}, trusted_apps={}",
|
"User {}: state={:?}, data={}, credential={:?}, tauth_grants={}",
|
||||||
diagnostics.user_id,
|
diagnostics.user_id,
|
||||||
diagnostics.local_state,
|
diagnostics.local_state,
|
||||||
diagnostics.data_present,
|
diagnostics.data_present,
|
||||||
diagnostics.credential_status,
|
diagnostics.credential_status,
|
||||||
diagnostics.trusted_app_count,
|
diagnostics.tauth_grant_count,
|
||||||
),
|
),
|
||||||
ResponsePayload::UserCredentialExport { .. } => {
|
ResponsePayload::UserCredentialExport { .. } => {
|
||||||
"Credential export payload withheld.".into()
|
"Credential export payload withheld.".into()
|
||||||
|
|
@ -588,8 +603,8 @@ impl IpcClient {
|
||||||
msg.push_str(&format!("\nDisplay Name: {name}"));
|
msg.push_str(&format!("\nDisplay Name: {name}"));
|
||||||
}
|
}
|
||||||
msg.push_str(&format!("\nCreated At: {}", user.created_at));
|
msg.push_str(&format!("\nCreated At: {}", user.created_at));
|
||||||
if !user.trusted_apps.is_empty() {
|
if !user.tauth_grants.is_empty() {
|
||||||
msg.push_str(&format!("\nTrusted Apps: {}", user.trusted_apps.join(", ")));
|
msg.push_str(&format!("\nTAuth grants: {}", user.tauth_grants.join(", ")));
|
||||||
}
|
}
|
||||||
msg
|
msg
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1030,7 +1030,7 @@ fn spawn_ipc_effect(
|
||||||
Ok(iota_ipc::ResponseResult::Ok(
|
Ok(iota_ipc::ResponseResult::Ok(
|
||||||
iota_ipc::ResponsePayload::UserDiagnostics(diagnostics),
|
iota_ipc::ResponsePayload::UserDiagnostics(diagnostics),
|
||||||
)) => Ok(format!(
|
)) => Ok(format!(
|
||||||
"Diagnostics: state={:?}, data={}, credential={}, trusted apps={}, pending={}",
|
"Diagnostics: state={:?}, data={}, credential={}, TAuth grants={}, pending={}",
|
||||||
diagnostics.local_state,
|
diagnostics.local_state,
|
||||||
if diagnostics.data_present {
|
if diagnostics.data_present {
|
||||||
"present"
|
"present"
|
||||||
|
|
@ -1038,7 +1038,7 @@ fn spawn_ipc_effect(
|
||||||
"empty"
|
"empty"
|
||||||
},
|
},
|
||||||
credential_status_label(diagnostics.credential_status),
|
credential_status_label(diagnostics.credential_status),
|
||||||
diagnostics.trusted_app_count,
|
diagnostics.tauth_grant_count,
|
||||||
diagnostics
|
diagnostics
|
||||||
.pending_operation
|
.pending_operation
|
||||||
.unwrap_or_else(|| "none".into()),
|
.unwrap_or_else(|| "none".into()),
|
||||||
|
|
|
||||||
|
|
@ -941,66 +941,6 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_create_app(cv: &CommunicationValue) -> CommunicationValue {
|
|
||||||
let sender_id = match required_sender_id(cv) {
|
|
||||||
Ok(sender_id) => sender_id,
|
|
||||||
Err(response) => return response,
|
|
||||||
};
|
|
||||||
let app_identifier = cv
|
|
||||||
.get_data(DataType::AppIdentifier)
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
let app_public_key = cv
|
|
||||||
.get_data(DataType::AppPublicKey)
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if !app_identifier.is_empty() && !app_public_key.is_empty() {
|
|
||||||
let user = match iota_storage::users::user_manager::get_user(sender_id) {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
|
||||||
};
|
|
||||||
if let Some(mut user) = user {
|
|
||||||
if !user.trusted_apps.contains_key(&app_identifier) {
|
|
||||||
user.trusted_apps.insert(app_identifier, app_public_key);
|
|
||||||
if iota_storage::users::user_manager::update_user(user).is_err() {
|
|
||||||
return error_response(cv, CommunicationType::ErrorInternal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CommunicationValue::new(CommunicationType::CreateApp)
|
|
||||||
.with_request_id(cv)
|
|
||||||
.with_receiver(sender_wire_id(sender_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_delete_app(cv: &CommunicationValue) -> CommunicationValue {
|
|
||||||
let sender_id = match required_sender_id(cv) {
|
|
||||||
Ok(sender_id) => sender_id,
|
|
||||||
Err(response) => return response,
|
|
||||||
};
|
|
||||||
let app_identifier = cv
|
|
||||||
.get_data(DataType::AppIdentifier)
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if !app_identifier.is_empty() {
|
|
||||||
if iota_storage::users::user_manager::revoke_trusted_app(sender_id, &app_identifier)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return error_response(cv, CommunicationType::ErrorInternal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CommunicationValue::new(CommunicationType::DeleteApp)
|
|
||||||
.with_request_id(cv)
|
|
||||||
.with_receiver(sender_wire_id(sender_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn contact_value(
|
fn contact_value(
|
||||||
contact: &iota_storage::users::contact::Contact,
|
contact: &iota_storage::users::contact::Contact,
|
||||||
messages: &[chat_files::StoredMessage],
|
messages: &[chat_files::StoredMessage],
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
|
base64 = "0.22"
|
||||||
|
hex = "0.4"
|
||||||
iota-ipc = { path = "../iota-ipc" }
|
iota-ipc = { path = "../iota-ipc" }
|
||||||
iota-auth = { path = "../iota-auth" }
|
iota-auth = { path = "../iota-auth" }
|
||||||
iota-connection = { path = "../iota-connection" }
|
iota-connection = { path = "../iota-connection" }
|
||||||
|
|
@ -20,9 +22,11 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "1f19a0d897c2
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
sysinfo = "0.38.0"
|
sysinfo = "0.38.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
tokio-util = { version = "0.7", features = ["rt"] }
|
||||||
|
url = "2"
|
||||||
uuid = { version = "*", features = ["v4"] }
|
uuid = { version = "*", features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,67 @@ impl CommandRouter {
|
||||||
};
|
};
|
||||||
ResponseResult::Ok(ResponsePayload::Users(users))
|
ResponseResult::Ok(ResponsePayload::Users(users))
|
||||||
}
|
}
|
||||||
|
LocalRequest::ListTAuthApps => match crate::tauth_apps::list() {
|
||||||
|
Ok(apps) => ResponseResult::Ok(ResponsePayload::TAuthApps(apps)),
|
||||||
|
Err(error) => {
|
||||||
|
log!("TAuth app listing failed: {error}");
|
||||||
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LocalRequest::GetTAuthApp { app_id } => match crate::tauth_apps::get(&app_id) {
|
||||||
|
Ok(Some(app)) => ResponseResult::Ok(ResponsePayload::TAuthApp(app)),
|
||||||
|
Ok(None) => ResponseResult::Error(IpcErrorCode::NotFound),
|
||||||
|
Err(error) => {
|
||||||
|
log!("TAuth app lookup failed: {error}");
|
||||||
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LocalRequest::CreateTAuthApp {
|
||||||
|
owner_user_id,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirects,
|
||||||
|
connection,
|
||||||
|
} => {
|
||||||
|
match crate::tauth_apps::create(owner_user_id, domain, name, redirects, connection)
|
||||||
|
{
|
||||||
|
Ok(created) => ResponseResult::Ok(ResponsePayload::TAuthAppCreated {
|
||||||
|
app: created.app,
|
||||||
|
credential: created.credential,
|
||||||
|
txt_record: created.txt_record,
|
||||||
|
manifest_template: created.manifest_template,
|
||||||
|
}),
|
||||||
|
Err(error) => {
|
||||||
|
log!("TAuth app creation failed: {error}");
|
||||||
|
ResponseResult::Error(IpcErrorCode::InvalidRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LocalRequest::ExportTAuthApp { app_id } => match crate::tauth_apps::export(&app_id) {
|
||||||
|
Ok(Some(credential)) => {
|
||||||
|
ResponseResult::Ok(ResponsePayload::TAuthAppCredentialExport {
|
||||||
|
app_id,
|
||||||
|
credential,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(None) => ResponseResult::Error(IpcErrorCode::NotFound),
|
||||||
|
Err(error) => {
|
||||||
|
log!("TAuth app export failed: {error}");
|
||||||
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LocalRequest::DeleteTAuthApp { app_id } => match crate::tauth_apps::delete(&app_id) {
|
||||||
|
Ok(true) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
||||||
|
message: format!(
|
||||||
|
"Deleted TAuth app {app_id}. Remove its DNS TXT record and HTTPS manifest."
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
Ok(false) => ResponseResult::Error(IpcErrorCode::NotFound),
|
||||||
|
Err(error) => {
|
||||||
|
log!("TAuth app deletion failed: {error}");
|
||||||
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
|
}
|
||||||
|
},
|
||||||
LocalRequest::CreateInvitation {
|
LocalRequest::CreateInvitation {
|
||||||
authority,
|
authority,
|
||||||
lifetime_seconds,
|
lifetime_seconds,
|
||||||
|
|
@ -730,33 +791,30 @@ impl CommandRouter {
|
||||||
},
|
},
|
||||||
data_present: residency.data_present,
|
data_present: residency.data_present,
|
||||||
credential_status,
|
credential_status,
|
||||||
trusted_app_count: profile
|
tauth_grant_count: iota_storage::tauth::list_grants(user_id)
|
||||||
.as_ref()
|
.map_or(0, |grants| grants.len()),
|
||||||
.map_or(0, |profile| profile.trusted_apps.len()),
|
|
||||||
pending_operation,
|
pending_operation,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
LocalRequest::RevokeTrustedApp { user_id, app_id } => {
|
LocalRequest::RevokeTAuthGrant { user_id, app_id } => {
|
||||||
match user_manager::revoke_trusted_app(user_id, &app_id) {
|
match user_manager::revoke_tauth_grant(user_id, &app_id) {
|
||||||
Ok(true) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
Ok(true) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
||||||
message: format!("Revoked trusted application {app_id} for user {user_id}"),
|
message: format!("Revoked TAuth grant {app_id} for user {user_id}"),
|
||||||
}),
|
}),
|
||||||
Ok(false) => ResponseResult::Error(IpcErrorCode::NotFound),
|
Ok(false) => ResponseResult::Error(IpcErrorCode::NotFound),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log!("Trusted application revocation failed for {user_id}: {error}");
|
log!("TAuth grant revocation failed for {user_id}: {error}");
|
||||||
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalRequest::RevokeAllTrustedApps { user_id } => {
|
LocalRequest::RevokeAllTAuthGrants { user_id } => {
|
||||||
match user_manager::revoke_all_trusted_apps(user_id) {
|
match user_manager::revoke_all_tauth_grants(user_id) {
|
||||||
Ok(removed) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
Ok(removed) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
||||||
message: format!(
|
message: format!("Revoked {removed} TAuth grants for user {user_id}"),
|
||||||
"Revoked {removed} trusted applications for user {user_id}"
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log!("Trusted application revocation failed for {user_id}: {error}");
|
log!("TAuth grant revocation failed for {user_id}: {error}");
|
||||||
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -935,7 +993,11 @@ impl CommandRouter {
|
||||||
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(),
|
tauth_grants: iota_storage::tauth::list_grants(user_id)
|
||||||
|
.map(|grants| {
|
||||||
|
grants.into_iter().map(|grant| grant.app_id).collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
state: iota_ipc::LocalUserState::Managed,
|
state: iota_ipc::LocalUserState::Managed,
|
||||||
data_present: residency.data_present,
|
data_present: residency.data_present,
|
||||||
credential_status,
|
credential_status,
|
||||||
|
|
@ -947,7 +1009,7 @@ impl CommandRouter {
|
||||||
username: residency.username,
|
username: residency.username,
|
||||||
display_name: None,
|
display_name: None,
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
trusted_apps: Vec::new(),
|
tauth_grants: Vec::new(),
|
||||||
state: iota_ipc::LocalUserState::Released,
|
state: iota_ipc::LocalUserState::Released,
|
||||||
data_present: residency.data_present,
|
data_present: residency.data_present,
|
||||||
credential_status: iota_ipc::CredentialStatus::None,
|
credential_status: iota_ipc::CredentialStatus::None,
|
||||||
|
|
@ -1055,11 +1117,11 @@ mod tests {
|
||||||
LocalRequest::ForceDetachUser { user_id: 1 },
|
LocalRequest::ForceDetachUser { user_id: 1 },
|
||||||
LocalRequest::ForgetReleasedUser { user_id: 1 },
|
LocalRequest::ForgetReleasedUser { user_id: 1 },
|
||||||
LocalRequest::GetUserDiagnostics { user_id: 1 },
|
LocalRequest::GetUserDiagnostics { user_id: 1 },
|
||||||
LocalRequest::RevokeTrustedApp {
|
LocalRequest::RevokeTAuthGrant {
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
app_id: "desktop".into(),
|
app_id: "desktop".into(),
|
||||||
},
|
},
|
||||||
LocalRequest::RevokeAllTrustedApps { user_id: 1 },
|
LocalRequest::RevokeAllTAuthGrants { user_id: 1 },
|
||||||
LocalRequest::ExportUserCredential { user_id: 1 },
|
LocalRequest::ExportUserCredential { user_id: 1 },
|
||||||
LocalRequest::PurgeUserData { user_id: 1 },
|
LocalRequest::PurgeUserData { user_id: 1 },
|
||||||
LocalRequest::ReleaseUser { user_id: 1 },
|
LocalRequest::ReleaseUser { user_id: 1 },
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub mod log_broadcaster;
|
||||||
pub mod log_buffer;
|
pub mod log_buffer;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
pub mod task_registry;
|
pub mod task_registry;
|
||||||
|
mod tauth_apps;
|
||||||
|
|
||||||
pub use accounts::{AccountAuthority, LocalIotaAccountAuthority, OmegaAccountAuthority};
|
pub use accounts::{AccountAuthority, LocalIotaAccountAuthority, OmegaAccountAuthority};
|
||||||
pub use command_router::{CommandRouter, IpcRole, PeerContext};
|
pub use command_router::{CommandRouter, IpcRole, PeerContext};
|
||||||
|
|
|
||||||
317
iota-daemon-lib/src/tauth_apps.rs
Normal file
317
iota-daemon-lib/src/tauth_apps.rs
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
|
use iota_ipc::{SecretString, TAuthAppSummary, TAuthConnectionInput};
|
||||||
|
use iota_storage::tauth::{self, OwnerApp};
|
||||||
|
use iota_util::crypto_helper::{public_key_bundle_from_base64, public_key_bundle_to_base64};
|
||||||
|
use iota_util::ta::{ConnectionMode, TaCredential};
|
||||||
|
use mtp::crypto::{Ed25519Signer, Keyring, SignatureScheme};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UnsignedOwnerCertificate<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
app_public_key: &'a str,
|
||||||
|
owner_principal: &'a str,
|
||||||
|
owner_public_key: &'a str,
|
||||||
|
owner_iota_id: u64,
|
||||||
|
issued_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct OwnerCertificate<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
app_public_key: &'a str,
|
||||||
|
owner_principal: &'a str,
|
||||||
|
owner_public_key: &'a str,
|
||||||
|
owner_iota_id: u64,
|
||||||
|
issued_at: i64,
|
||||||
|
signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UnsignedManifest<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
public_key: &'a str,
|
||||||
|
name: &'a str,
|
||||||
|
domain: &'a str,
|
||||||
|
redirects: &'a [String],
|
||||||
|
owner_certificate: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Manifest<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
public_key: &'a str,
|
||||||
|
name: &'a str,
|
||||||
|
domain: &'a str,
|
||||||
|
redirects: &'a [String],
|
||||||
|
owner_certificate: &'a str,
|
||||||
|
signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreatedApp {
|
||||||
|
pub app: TAuthAppSummary,
|
||||||
|
pub credential: SecretString,
|
||||||
|
pub txt_record: String,
|
||||||
|
pub manifest_template: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list() -> Result<Vec<TAuthAppSummary>, String> {
|
||||||
|
tauth::list_owner_apps()
|
||||||
|
.map(|apps| apps.into_iter().map(summary).collect())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(app_id: &str) -> Result<Option<TAuthAppSummary>, String> {
|
||||||
|
tauth::get_owner_app(app_id)
|
||||||
|
.map(|app| app.map(summary))
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export(app_id: &str) -> Result<Option<SecretString>, String> {
|
||||||
|
iota_util::file_util::read_tauth_credential(app_id)
|
||||||
|
.map(|credential| credential.map(|bytes| SecretString(STANDARD.encode(bytes))))
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(app_id: &str) -> Result<bool, String> {
|
||||||
|
if tauth::get_owner_app(app_id)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
iota_util::file_util::remove_tauth_credential(app_id).map_err(|error| error.to_string())?;
|
||||||
|
tauth::delete_owner_app(app_id).map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(
|
||||||
|
owner_user_id: i64,
|
||||||
|
domain: String,
|
||||||
|
name: String,
|
||||||
|
redirects: Vec<String>,
|
||||||
|
connection: TAuthConnectionInput,
|
||||||
|
) -> Result<CreatedApp, String> {
|
||||||
|
let domain = canonical_domain(domain)?;
|
||||||
|
let name = name.trim().to_owned();
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err("app name is empty".into());
|
||||||
|
}
|
||||||
|
let redirects = redirects
|
||||||
|
.into_iter()
|
||||||
|
.map(|redirect| validate_redirect(&redirect).map(|()| redirect))
|
||||||
|
.collect::<Result<BTreeSet<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if redirects.is_empty() {
|
||||||
|
return Err("at least one redirect is required".into());
|
||||||
|
}
|
||||||
|
let owner_iota_id = iota_storage::util::config_util::CONFIG
|
||||||
|
.load()
|
||||||
|
.iota_id
|
||||||
|
.ok_or_else(|| "Iota ID is not configured".to_string())?;
|
||||||
|
let owner = iota_storage::users::user_manager::get_user(owner_user_id)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.ok_or_else(|| "owner user does not exist".to_string())?;
|
||||||
|
let owner_credential =
|
||||||
|
iota_util::file_util::read_user_credential_with_legacy(owner_user_id, &owner.username)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.ok_or_else(|| "owner user credential is unavailable".to_string())?;
|
||||||
|
let owner_credential =
|
||||||
|
iota_util::tu::TuCredential::parse(&owner_credential).map_err(|error| error.to_string())?;
|
||||||
|
let owner_principal = owner_credential
|
||||||
|
.principal()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let owner_principal = format!(
|
||||||
|
"{}#{}",
|
||||||
|
owner_principal.authority.as_str(),
|
||||||
|
owner_principal.user_id
|
||||||
|
);
|
||||||
|
let owner_public_key = public_key_bundle_to_base64(&owner_credential.public_key_bundle());
|
||||||
|
|
||||||
|
let keyring = Keyring::generate();
|
||||||
|
let app_id = iota_util::ta::app_id(&keyring.public_key_bundle());
|
||||||
|
let app_public_key = public_key_bundle_to_base64(&keyring.public_key_bundle());
|
||||||
|
let issued_at = tauth::now_seconds();
|
||||||
|
let unsigned_certificate = UnsignedOwnerCertificate {
|
||||||
|
version: 1,
|
||||||
|
app_id: &app_id,
|
||||||
|
app_public_key: &app_public_key,
|
||||||
|
owner_principal: &owner_principal,
|
||||||
|
owner_public_key: &owner_public_key,
|
||||||
|
owner_iota_id,
|
||||||
|
issued_at,
|
||||||
|
};
|
||||||
|
let owner_signer = Ed25519Signer::new(&owner_credential.keyring.sig_cl_secret_key)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let owner_signature = owner_signer
|
||||||
|
.sign(&serde_json::to_vec(&unsigned_certificate).map_err(|error| error.to_string())?)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let certificate = OwnerCertificate {
|
||||||
|
version: unsigned_certificate.version,
|
||||||
|
app_id: unsigned_certificate.app_id,
|
||||||
|
app_public_key: unsigned_certificate.app_public_key,
|
||||||
|
owner_principal: unsigned_certificate.owner_principal,
|
||||||
|
owner_public_key: unsigned_certificate.owner_public_key,
|
||||||
|
owner_iota_id: unsigned_certificate.owner_iota_id,
|
||||||
|
issued_at: unsigned_certificate.issued_at,
|
||||||
|
signature: STANDARD.encode(owner_signature),
|
||||||
|
};
|
||||||
|
let certificate_bytes = serde_json::to_vec(&certificate).map_err(|error| error.to_string())?;
|
||||||
|
let owner_certificate = STANDARD.encode(&certificate_bytes);
|
||||||
|
|
||||||
|
let unsigned_manifest = UnsignedManifest {
|
||||||
|
version: 1,
|
||||||
|
app_id: &app_id,
|
||||||
|
public_key: &app_public_key,
|
||||||
|
name: &name,
|
||||||
|
domain: &domain,
|
||||||
|
redirects: &redirects,
|
||||||
|
owner_certificate: &owner_certificate,
|
||||||
|
};
|
||||||
|
let app_signer =
|
||||||
|
Ed25519Signer::new(&keyring.sig_cl_secret_key).map_err(|error| error.to_string())?;
|
||||||
|
let manifest_signature = app_signer
|
||||||
|
.sign(&serde_json::to_vec(&unsigned_manifest).map_err(|error| error.to_string())?)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let manifest = Manifest {
|
||||||
|
version: unsigned_manifest.version,
|
||||||
|
app_id: unsigned_manifest.app_id,
|
||||||
|
public_key: unsigned_manifest.public_key,
|
||||||
|
name: unsigned_manifest.name,
|
||||||
|
domain: unsigned_manifest.domain,
|
||||||
|
redirects: unsigned_manifest.redirects,
|
||||||
|
owner_certificate: unsigned_manifest.owner_certificate,
|
||||||
|
signature: STANDARD.encode(manifest_signature),
|
||||||
|
};
|
||||||
|
let manifest_template =
|
||||||
|
serde_json::to_string_pretty(&manifest).map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let (mode, connection_mode, endpoint_url, omikron_public_key) = match connection {
|
||||||
|
TAuthConnectionInput::Hosted { omega_url } => {
|
||||||
|
let omega_url = validate_https(&omega_url)?;
|
||||||
|
(
|
||||||
|
ConnectionMode::Hosted {
|
||||||
|
omega_url: omega_url.clone(),
|
||||||
|
owner_iota_id,
|
||||||
|
},
|
||||||
|
"hosted".to_owned(),
|
||||||
|
omega_url.to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TAuthConnectionInput::ForcedOmikron {
|
||||||
|
omikron_url,
|
||||||
|
omikron_public_key,
|
||||||
|
} => {
|
||||||
|
let omikron_url = validate_https(&omikron_url)?;
|
||||||
|
let key = public_key_bundle_from_base64(&omikron_public_key)
|
||||||
|
.ok_or_else(|| "forced Omikron public key is invalid".to_string())?;
|
||||||
|
(
|
||||||
|
ConnectionMode::ForcedOmikron {
|
||||||
|
omikron_url: omikron_url.clone(),
|
||||||
|
omikron_public_key: key,
|
||||||
|
owner_iota_id,
|
||||||
|
},
|
||||||
|
"forced_omikron".to_owned(),
|
||||||
|
omikron_url.to_string(),
|
||||||
|
Some(omikron_public_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let credential = TaCredential {
|
||||||
|
keyring,
|
||||||
|
owner_certificate: certificate_bytes,
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
let credential_bytes = credential.to_bytes().map_err(|error| error.to_string())?;
|
||||||
|
let credential_path =
|
||||||
|
iota_util::file_util::tauth_credential_path(&app_id).map_err(|error| error.to_string())?;
|
||||||
|
credential
|
||||||
|
.export(&credential_path)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let app = OwnerApp {
|
||||||
|
app_id: app_id.clone(),
|
||||||
|
owner_user_id,
|
||||||
|
domain: domain.clone(),
|
||||||
|
public_key: app_public_key,
|
||||||
|
owner_certificate,
|
||||||
|
connection_mode,
|
||||||
|
endpoint_url,
|
||||||
|
omikron_public_key,
|
||||||
|
created_at: issued_at,
|
||||||
|
};
|
||||||
|
if let Err(error) = tauth::register_owner_app(&app) {
|
||||||
|
let _ = iota_util::file_util::remove_tauth_credential(&app_id);
|
||||||
|
return Err(error.to_string());
|
||||||
|
}
|
||||||
|
let manifest_hash = hex::encode(mtp::crypto::sha256(manifest_template.as_bytes()));
|
||||||
|
Ok(CreatedApp {
|
||||||
|
app: summary(app),
|
||||||
|
credential: SecretString(STANDARD.encode(credential_bytes)),
|
||||||
|
txt_record: format!("v=TAUTH1;app={app_id};manifest={manifest_hash}"),
|
||||||
|
manifest_template,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(app: OwnerApp) -> TAuthAppSummary {
|
||||||
|
TAuthAppSummary {
|
||||||
|
app_id: app.app_id,
|
||||||
|
owner_user_id: app.owner_user_id,
|
||||||
|
domain: app.domain,
|
||||||
|
public_key: app.public_key,
|
||||||
|
connection_mode: app.connection_mode,
|
||||||
|
endpoint_url: app.endpoint_url,
|
||||||
|
created_at: app.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonical_domain(domain: String) -> Result<String, String> {
|
||||||
|
let domain = domain.trim().to_ascii_lowercase();
|
||||||
|
if domain.is_empty()
|
||||||
|
|| domain.contains(['/', ':'])
|
||||||
|
|| domain.split('.').any(|label| {
|
||||||
|
label.is_empty()
|
||||||
|
|| label.starts_with('-')
|
||||||
|
|| label.ends_with('-')
|
||||||
|
|| !label
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err("app domain is invalid".into());
|
||||||
|
}
|
||||||
|
Ok(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_redirect(value: &str) -> Result<(), String> {
|
||||||
|
let url = Url::parse(value).map_err(|error| error.to_string())?;
|
||||||
|
let loopback = url.scheme() == "http"
|
||||||
|
&& matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "[::1]"));
|
||||||
|
if url.host_str().is_none()
|
||||||
|
|| !url.username().is_empty()
|
||||||
|
|| url.password().is_some()
|
||||||
|
|| url.fragment().is_some()
|
||||||
|
|| url
|
||||||
|
.query_pairs()
|
||||||
|
.any(|(key, _)| matches!(key.as_ref(), "code" | "state" | "locator" | "error"))
|
||||||
|
|| (url.scheme() != "https" && !loopback)
|
||||||
|
{
|
||||||
|
return Err("redirect must be exact HTTPS, or HTTP loopback, without a fragment".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_https(value: &str) -> Result<Url, String> {
|
||||||
|
let url = Url::parse(value).map_err(|error| error.to_string())?;
|
||||||
|
if url.scheme() != "https" || url.host_str().is_none() {
|
||||||
|
return Err("connection URL must use HTTPS".into());
|
||||||
|
}
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,9 @@ pub use protocol::{
|
||||||
LocalRequest, LocalUserState, LogEntriesResponse, LogEntry, MetricSample,
|
LocalRequest, LocalUserState, LogEntriesResponse, LogEntry, MetricSample,
|
||||||
OmikronStatusResponse, ReconcileAction, RequestEnvelope, ResponseEnvelope, ResponsePayload,
|
OmikronStatusResponse, ReconcileAction, RequestEnvelope, ResponseEnvelope, ResponsePayload,
|
||||||
ResponseResult, SecretString, StartupPhase, StateSnapshot, StatusResponse, SupervisorKind,
|
ResponseResult, SecretString, StartupPhase, StateSnapshot, StatusResponse, SupervisorKind,
|
||||||
TaskSummary, TuCredentialPreview, UpdateStatusResponse, UserDetailResponse, UserDiagnostics,
|
TAuthAppSummary, TAuthConnectionInput, TaskSummary, TuCredentialPreview, UpdateStatusResponse,
|
||||||
UserOperationKind, UserOperationSummary, UserReconcileResult, UserSummary,
|
UserDetailResponse, UserDiagnostics, UserOperationKind, UserOperationSummary,
|
||||||
|
UserReconcileResult, UserSummary,
|
||||||
};
|
};
|
||||||
pub use transport::{MAX_MESSAGE_SIZE, read_msg, write_msg};
|
pub use transport::{MAX_MESSAGE_SIZE, read_msg, write_msg};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,23 @@ pub enum LocalRequest {
|
||||||
GetStatus,
|
GetStatus,
|
||||||
ListTasks,
|
ListTasks,
|
||||||
ListUsers,
|
ListUsers,
|
||||||
|
ListTAuthApps,
|
||||||
|
GetTAuthApp {
|
||||||
|
app_id: String,
|
||||||
|
},
|
||||||
|
CreateTAuthApp {
|
||||||
|
owner_user_id: i64,
|
||||||
|
domain: String,
|
||||||
|
name: String,
|
||||||
|
redirects: Vec<String>,
|
||||||
|
connection: TAuthConnectionInput,
|
||||||
|
},
|
||||||
|
ExportTAuthApp {
|
||||||
|
app_id: String,
|
||||||
|
},
|
||||||
|
DeleteTAuthApp {
|
||||||
|
app_id: String,
|
||||||
|
},
|
||||||
CreateInvitation {
|
CreateInvitation {
|
||||||
authority: InvitationAuthority,
|
authority: InvitationAuthority,
|
||||||
lifetime_seconds: u64,
|
lifetime_seconds: u64,
|
||||||
|
|
@ -79,11 +96,11 @@ pub enum LocalRequest {
|
||||||
GetUserDiagnostics {
|
GetUserDiagnostics {
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
},
|
},
|
||||||
RevokeTrustedApp {
|
RevokeTAuthGrant {
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
app_id: String,
|
app_id: String,
|
||||||
},
|
},
|
||||||
RevokeAllTrustedApps {
|
RevokeAllTAuthGrants {
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
},
|
},
|
||||||
ExportUserCredential {
|
ExportUserCredential {
|
||||||
|
|
@ -161,6 +178,8 @@ impl LocalRequest {
|
||||||
Self::GetStatus
|
Self::GetStatus
|
||||||
| Self::ListTasks
|
| Self::ListTasks
|
||||||
| Self::ListUsers
|
| Self::ListUsers
|
||||||
|
| Self::ListTAuthApps
|
||||||
|
| Self::GetTAuthApp { .. }
|
||||||
| Self::ListInvitations { .. }
|
| Self::ListInvitations { .. }
|
||||||
| Self::GetDaemonStatus
|
| Self::GetDaemonStatus
|
||||||
| Self::GetOmikronStatus
|
| Self::GetOmikronStatus
|
||||||
|
|
@ -174,6 +193,9 @@ impl LocalRequest {
|
||||||
Self::ReconnectOmikron | Self::ReloadConfig => IpcRole::Operate,
|
Self::ReconnectOmikron | Self::ReloadConfig => IpcRole::Operate,
|
||||||
|
|
||||||
Self::CreateUser { .. }
|
Self::CreateUser { .. }
|
||||||
|
| Self::CreateTAuthApp { .. }
|
||||||
|
| Self::ExportTAuthApp { .. }
|
||||||
|
| Self::DeleteTAuthApp { .. }
|
||||||
| Self::CreateInvitation { .. }
|
| Self::CreateInvitation { .. }
|
||||||
| Self::RevokeInvitation { .. }
|
| Self::RevokeInvitation { .. }
|
||||||
| Self::InspectTuCredential { .. }
|
| Self::InspectTuCredential { .. }
|
||||||
|
|
@ -181,8 +203,8 @@ impl LocalRequest {
|
||||||
| Self::ReconcileUser { .. }
|
| Self::ReconcileUser { .. }
|
||||||
| Self::ForceDetachUser { .. }
|
| Self::ForceDetachUser { .. }
|
||||||
| Self::ForgetReleasedUser { .. }
|
| Self::ForgetReleasedUser { .. }
|
||||||
| Self::RevokeTrustedApp { .. }
|
| Self::RevokeTAuthGrant { .. }
|
||||||
| Self::RevokeAllTrustedApps { .. }
|
| Self::RevokeAllTAuthGrants { .. }
|
||||||
| Self::ExportUserCredential { .. }
|
| Self::ExportUserCredential { .. }
|
||||||
| Self::PurgeUserData { .. }
|
| Self::PurgeUserData { .. }
|
||||||
| Self::ReleaseUser { .. }
|
| Self::ReleaseUser { .. }
|
||||||
|
|
@ -265,6 +287,18 @@ pub enum ResponsePayload {
|
||||||
Status(StatusResponse),
|
Status(StatusResponse),
|
||||||
Tasks(Vec<TaskSummary>),
|
Tasks(Vec<TaskSummary>),
|
||||||
Users(Vec<UserSummary>),
|
Users(Vec<UserSummary>),
|
||||||
|
TAuthApps(Vec<TAuthAppSummary>),
|
||||||
|
TAuthApp(TAuthAppSummary),
|
||||||
|
TAuthAppCreated {
|
||||||
|
app: TAuthAppSummary,
|
||||||
|
credential: SecretString,
|
||||||
|
txt_record: String,
|
||||||
|
manifest_template: String,
|
||||||
|
},
|
||||||
|
TAuthAppCredentialExport {
|
||||||
|
app_id: String,
|
||||||
|
credential: SecretString,
|
||||||
|
},
|
||||||
InvitationCreated(InvitationCreated),
|
InvitationCreated(InvitationCreated),
|
||||||
Invitations(Vec<InvitationSummary>),
|
Invitations(Vec<InvitationSummary>),
|
||||||
InvitationUpdated(InvitationSummary),
|
InvitationUpdated(InvitationSummary),
|
||||||
|
|
@ -311,6 +345,29 @@ pub struct OmikronStatusResponse {
|
||||||
pub iota_id: Option<u64>,
|
pub iota_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "mode", rename_all = "snake_case")]
|
||||||
|
pub enum TAuthConnectionInput {
|
||||||
|
Hosted {
|
||||||
|
omega_url: String,
|
||||||
|
},
|
||||||
|
ForcedOmikron {
|
||||||
|
omikron_url: String,
|
||||||
|
omikron_public_key: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct TAuthAppSummary {
|
||||||
|
pub app_id: String,
|
||||||
|
pub owner_user_id: i64,
|
||||||
|
pub domain: String,
|
||||||
|
pub public_key: String,
|
||||||
|
pub connection_mode: String,
|
||||||
|
pub endpoint_url: String,
|
||||||
|
pub created_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct ComponentStatusResponse {
|
pub struct ComponentStatusResponse {
|
||||||
pub id: ComponentId,
|
pub id: ComponentId,
|
||||||
|
|
@ -324,7 +381,7 @@ pub struct UserDetailResponse {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub trusted_apps: Vec<String>,
|
pub tauth_grants: Vec<String>,
|
||||||
pub state: LocalUserState,
|
pub state: LocalUserState,
|
||||||
pub data_present: bool,
|
pub data_present: bool,
|
||||||
pub credential_status: CredentialStatus,
|
pub credential_status: CredentialStatus,
|
||||||
|
|
@ -467,7 +524,7 @@ pub struct UserDiagnostics {
|
||||||
pub local_state: LocalUserState,
|
pub local_state: LocalUserState,
|
||||||
pub data_present: bool,
|
pub data_present: bool,
|
||||||
pub credential_status: CredentialStatus,
|
pub credential_status: CredentialStatus,
|
||||||
pub trusted_app_count: usize,
|
pub tauth_grant_count: usize,
|
||||||
pub pending_operation: Option<String>,
|
pub pending_operation: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ pub const COMMANDS: &[&str] = &[
|
||||||
"users forget ",
|
"users forget ",
|
||||||
"users apps revoke ",
|
"users apps revoke ",
|
||||||
"users apps revoke-all ",
|
"users apps revoke-all ",
|
||||||
|
"apps list",
|
||||||
|
"apps show ",
|
||||||
|
"apps create hosted ",
|
||||||
|
"apps create forced-omikron ",
|
||||||
|
"apps delete ",
|
||||||
"omikron status",
|
"omikron status",
|
||||||
"reconnect",
|
"reconnect",
|
||||||
"identity rotate",
|
"identity rotate",
|
||||||
|
|
@ -97,16 +102,61 @@ pub fn parse(line: &str) -> Option<LocalRequest> {
|
||||||
user_id: id_str.parse::<i64>().ok()?,
|
user_id: id_str.parse::<i64>().ok()?,
|
||||||
}),
|
}),
|
||||||
["user" | "users", "apps", "revoke", id_str, app_id] => {
|
["user" | "users", "apps", "revoke", id_str, app_id] => {
|
||||||
Some(LocalRequest::RevokeTrustedApp {
|
Some(LocalRequest::RevokeTAuthGrant {
|
||||||
user_id: id_str.parse::<i64>().ok()?,
|
user_id: id_str.parse::<i64>().ok()?,
|
||||||
app_id: app_id.to_string(),
|
app_id: app_id.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
["user" | "users", "apps", "revoke-all", id_str] => {
|
["user" | "users", "apps", "revoke-all", id_str] => {
|
||||||
Some(LocalRequest::RevokeAllTrustedApps {
|
Some(LocalRequest::RevokeAllTAuthGrants {
|
||||||
user_id: id_str.parse::<i64>().ok()?,
|
user_id: id_str.parse::<i64>().ok()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
["apps", "list"] => Some(LocalRequest::ListTAuthApps),
|
||||||
|
["apps", "show", app_id] => Some(LocalRequest::GetTAuthApp {
|
||||||
|
app_id: app_id.to_string(),
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
"apps",
|
||||||
|
"create",
|
||||||
|
"hosted",
|
||||||
|
owner,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirect,
|
||||||
|
omega_url,
|
||||||
|
] => Some(LocalRequest::CreateTAuthApp {
|
||||||
|
owner_user_id: owner.parse().ok()?,
|
||||||
|
domain: domain.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
redirects: vec![redirect.to_string()],
|
||||||
|
connection: crate::TAuthConnectionInput::Hosted {
|
||||||
|
omega_url: omega_url.to_string(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
"apps",
|
||||||
|
"create",
|
||||||
|
"forced-omikron",
|
||||||
|
owner,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirect,
|
||||||
|
omikron_url,
|
||||||
|
omikron_public_key,
|
||||||
|
] => Some(LocalRequest::CreateTAuthApp {
|
||||||
|
owner_user_id: owner.parse().ok()?,
|
||||||
|
domain: domain.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
redirects: vec![redirect.to_string()],
|
||||||
|
connection: crate::TAuthConnectionInput::ForcedOmikron {
|
||||||
|
omikron_url: omikron_url.to_string(),
|
||||||
|
omikron_public_key: omikron_public_key.to_string(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
["apps", "delete", app_id, "confirm"] => Some(LocalRequest::DeleteTAuthApp {
|
||||||
|
app_id: app_id.to_string(),
|
||||||
|
}),
|
||||||
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
|
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
|
||||||
["regenerate", "keys"] | ["identity", "rotate"] => Some(LocalRequest::RotateIotaIdentity),
|
["regenerate", "keys"] | ["identity", "rotate"] => Some(LocalRequest::RotateIotaIdentity),
|
||||||
["reload"] | ["restart"] => Some(LocalRequest::RequestProcessExit {
|
["reload"] | ["restart"] => Some(LocalRequest::RequestProcessExit {
|
||||||
|
|
@ -174,11 +224,11 @@ mod tests {
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse("users apps revoke 42 desktop"),
|
parse("users apps revoke 42 desktop"),
|
||||||
Some(LocalRequest::RevokeTrustedApp { user_id: 42, .. })
|
Some(LocalRequest::RevokeTAuthGrant { user_id: 42, .. })
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse("users apps revoke-all 42"),
|
parse("users apps revoke-all 42"),
|
||||||
Some(LocalRequest::RevokeAllTrustedApps { user_id: 42 })
|
Some(LocalRequest::RevokeAllTAuthGrants { user_id: 42 })
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse("identity rotate"),
|
parse("identity rotate"),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ r2d2 = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
sha2 = "0.10"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rusqlite = "0.40.0"
|
rusqlite = "0.40.0"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod node_directory;
|
pub mod node_directory;
|
||||||
pub mod storage_error;
|
pub mod storage_error;
|
||||||
|
pub mod tauth;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
|
||||||
1005
iota-storage/src/tauth.rs
Normal file
1005
iota-storage/src/tauth.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::users::pending_operations;
|
use crate::users::pending_operations;
|
||||||
use crate::users::user_profile::UserProfile;
|
use crate::users::user_profile::UserProfile;
|
||||||
use crate::util::db;
|
use crate::util::db;
|
||||||
use iota_util::file_util::{delete_user_directory, load_file, remove_user_credential, save_file};
|
use iota_util::file_util::{delete_user_directory, load_file, remove_user_credential};
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
|
@ -113,15 +113,6 @@ fn persist_user_profile(
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for (app_id, app_secret) in &user.trusted_apps {
|
|
||||||
tx.execute(
|
|
||||||
r#"
|
|
||||||
INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret)
|
|
||||||
VALUES (?1, ?2, ?3)
|
|
||||||
"#,
|
|
||||||
params![user.user_id, app_id, app_secret],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +133,6 @@ pub fn get_user_by_username(
|
||||||
private_key_hash: r.get(3)?,
|
private_key_hash: r.get(3)?,
|
||||||
created_at: r.get(5)?,
|
created_at: r.get(5)?,
|
||||||
reset_token: r.get(4)?,
|
reset_token: r.get(4)?,
|
||||||
trusted_apps: std::collections::HashMap::new(),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
@ -151,11 +141,7 @@ pub fn get_user_by_username(
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
user.map(|mut user| {
|
Ok(user)
|
||||||
user.trusted_apps = load_trusted_apps(user.user_id)?;
|
|
||||||
Ok(user)
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_error::StorageError> {
|
pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_error::StorageError> {
|
||||||
|
|
@ -173,7 +159,6 @@ pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_erro
|
||||||
private_key_hash: r.get(3)?,
|
private_key_hash: r.get(3)?,
|
||||||
created_at: r.get(5)?,
|
created_at: r.get(5)?,
|
||||||
reset_token: r.get(4)?,
|
reset_token: r.get(4)?,
|
||||||
trusted_apps: std::collections::HashMap::new(),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
@ -182,11 +167,7 @@ pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_erro
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
user.map(|mut user| {
|
Ok(user)
|
||||||
user.trusted_apps = load_trusted_apps(user_id)?;
|
|
||||||
Ok(user)
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_users() -> Result<Vec<UserProfile>, crate::storage_error::StorageError> {
|
pub fn get_users() -> Result<Vec<UserProfile>, crate::storage_error::StorageError> {
|
||||||
|
|
@ -216,71 +197,51 @@ pub fn get_users() -> Result<Vec<UserProfile>, crate::storage_error::StorageErro
|
||||||
private_key_hash,
|
private_key_hash,
|
||||||
created_at,
|
created_at,
|
||||||
reset_token,
|
reset_token,
|
||||||
trusted_apps: std::collections::HashMap::new(),
|
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let mut user = row?;
|
out.push(row?);
|
||||||
user.trusted_apps = load_trusted_apps_from(conn, user.user_id)?;
|
|
||||||
out.push(user);
|
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_trusted_apps(
|
pub fn revoke_tauth_grant(
|
||||||
user_id: i64,
|
|
||||||
) -> Result<std::collections::HashMap<String, String>, crate::storage_error::StorageError> {
|
|
||||||
db::with_db(|conn| load_trusted_apps_from(conn, user_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_trusted_apps_from(
|
|
||||||
conn: &rusqlite::Connection,
|
|
||||||
user_id: i64,
|
|
||||||
) -> Result<std::collections::HashMap<String, String>, crate::storage_error::StorageError> {
|
|
||||||
let mut stmt =
|
|
||||||
conn.prepare("SELECT app_id, app_secret FROM trusted_apps WHERE user_id = ?1")?;
|
|
||||||
let rows = stmt.query_map(params![user_id], |r| {
|
|
||||||
Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut map = std::collections::HashMap::new();
|
|
||||||
for row in rows {
|
|
||||||
let (key, value) = row?;
|
|
||||||
map.insert(key, value);
|
|
||||||
}
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn revoke_trusted_app(
|
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
app_id: &str,
|
app_id: &str,
|
||||||
) -> Result<bool, crate::storage_error::StorageError> {
|
) -> Result<bool, crate::storage_error::StorageError> {
|
||||||
db::with_immediate_transaction(|tx| {
|
crate::tauth::revoke_grant(user_id, app_id)
|
||||||
let removed = tx.execute(
|
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))
|
||||||
"DELETE FROM trusted_apps WHERE user_id = ?1 AND app_id = ?2",
|
|
||||||
params![user_id, app_id],
|
|
||||||
)?;
|
|
||||||
Ok(removed > 0)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revoke_all_trusted_apps(user_id: i64) -> Result<usize, crate::storage_error::StorageError> {
|
pub fn revoke_all_tauth_grants(user_id: i64) -> Result<usize, crate::storage_error::StorageError> {
|
||||||
db::with_immediate_transaction(|tx| {
|
let grants = crate::tauth::list_grants(user_id)
|
||||||
let removed = tx.execute(
|
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))?;
|
||||||
"DELETE FROM trusted_apps WHERE user_id = ?1",
|
for grant in &grants {
|
||||||
params![user_id],
|
crate::tauth::revoke_grant(user_id, &grant.app_id)
|
||||||
)?;
|
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))?;
|
||||||
Ok(removed)
|
}
|
||||||
})
|
Ok(grants.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_user(user_id: i64) -> Result<(), crate::storage_error::StorageError> {
|
pub fn remove_user(user_id: i64) -> Result<(), crate::storage_error::StorageError> {
|
||||||
db::with_immediate_transaction(|conn| {
|
db::with_immediate_transaction(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"DELETE FROM trusted_apps WHERE user_id = ?1",
|
"DELETE FROM tauth_codes WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tauth_sessions WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tauth_metadata WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tauth_grants WHERE local_user_id = ?1",
|
||||||
params![user_id],
|
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])?;
|
||||||
|
|
@ -321,7 +282,19 @@ pub fn finalize_local_release(
|
||||||
db::with_db(|conn| {
|
db::with_db(|conn| {
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.unchecked_transaction()?;
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"DELETE FROM trusted_apps WHERE user_id = ?1",
|
"DELETE FROM tauth_codes WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM tauth_sessions WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM tauth_metadata WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM tauth_grants WHERE local_user_id = ?1",
|
||||||
params![user_id],
|
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])?;
|
||||||
|
|
@ -586,7 +559,19 @@ pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::Stor
|
||||||
purge_user_data(user_id)?;
|
purge_user_data(user_id)?;
|
||||||
db::with_db(|conn| {
|
db::with_db(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"DELETE FROM trusted_apps WHERE user_id = ?1",
|
"DELETE FROM tauth_codes WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tauth_sessions WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tauth_metadata WHERE local_user_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tauth_grants WHERE local_user_id = ?1",
|
||||||
params![user_id],
|
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])?;
|
||||||
|
|
@ -658,7 +643,7 @@ pub fn get_residency_by_id(
|
||||||
pub fn clear() -> Result<(), crate::storage_error::StorageError> {
|
pub fn clear() -> Result<(), crate::storage_error::StorageError> {
|
||||||
db::with_immediate_transaction(|conn| {
|
db::with_immediate_transaction(|conn| {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"DELETE FROM trusted_apps; DELETE FROM users; DELETE FROM user_residency;",
|
"DELETE FROM tauth_codes; DELETE FROM tauth_sessions; DELETE FROM tauth_metadata; DELETE FROM tauth_grants; DELETE FROM users; DELETE FROM user_residency;",
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|
@ -778,12 +763,6 @@ mod tests {
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
display_name TEXT
|
display_name TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE trusted_apps (
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
app_id TEXT NOT NULL,
|
|
||||||
app_secret TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (user_id, app_id)
|
|
||||||
);
|
|
||||||
CREATE TABLE user_residency (
|
CREATE TABLE user_residency (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
|
|
@ -797,7 +776,7 @@ mod tests {
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut user = UserProfile::new_with_created_at(
|
let user = UserProfile::new_with_created_at(
|
||||||
1,
|
1,
|
||||||
"alice".into(),
|
"alice".into(),
|
||||||
Some("Alice".into()),
|
Some("Alice".into()),
|
||||||
|
|
@ -806,8 +785,6 @@ mod tests {
|
||||||
None,
|
None,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
user.trusted_apps.insert("app".into(), "secret".into());
|
|
||||||
|
|
||||||
let transaction = connection.transaction().unwrap();
|
let transaction = connection.transaction().unwrap();
|
||||||
persist_user_profile(&transaction, &user).unwrap();
|
persist_user_profile(&transaction, &user).unwrap();
|
||||||
transaction.commit().unwrap();
|
transaction.commit().unwrap();
|
||||||
|
|
@ -829,25 +806,5 @@ mod tests {
|
||||||
42,
|
42,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let trusted_app_count: i64 = connection
|
|
||||||
.query_row(
|
|
||||||
"SELECT COUNT(*) FROM trusted_apps WHERE user_id = ?1",
|
|
||||||
params![1],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(trusted_app_count, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_app_data(user_id: i64, app_identifier: &str, data: &str) {
|
|
||||||
let path = format!("users/{}/apps", user_id);
|
|
||||||
let name = format!("{}.json", app_identifier);
|
|
||||||
save_file(&path, &name, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_app_data(user_id: i64, app_identifier: &str) -> String {
|
|
||||||
let path = format!("users/{}/apps", user_id);
|
|
||||||
let name = format!("{}.json", app_identifier);
|
|
||||||
load_file(&path, &name)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ pub struct UserProfile {
|
||||||
pub reset_token: Option<String>,
|
pub reset_token: Option<String>,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub trusted_apps: std::collections::HashMap<String, String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserProfile {
|
impl UserProfile {
|
||||||
|
|
@ -59,7 +58,6 @@ impl UserProfile {
|
||||||
private_key_hash,
|
private_key_hash,
|
||||||
created_at,
|
created_at,
|
||||||
reset_token,
|
reset_token,
|
||||||
trusted_apps: std::collections::HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,15 +90,6 @@ impl UserProfile {
|
||||||
let created_at = j["created_at"].as_i64()?;
|
let created_at = j["created_at"].as_i64()?;
|
||||||
let display_name = j["display_name"].as_str().map(|s| s.to_string());
|
let display_name = j["display_name"].as_str().map(|s| s.to_string());
|
||||||
|
|
||||||
let mut trusted_apps = std::collections::HashMap::new();
|
|
||||||
if j["trusted_apps"].is_object() {
|
|
||||||
for (key, value) in j["trusted_apps"].entries() {
|
|
||||||
if let Some(s) = value.as_str() {
|
|
||||||
trusted_apps.insert(key.to_string(), s.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(UserProfile {
|
Some(UserProfile {
|
||||||
user_id,
|
user_id,
|
||||||
username,
|
username,
|
||||||
|
|
@ -109,7 +98,6 @@ impl UserProfile {
|
||||||
private_key_hash,
|
private_key_hash,
|
||||||
created_at,
|
created_at,
|
||||||
reset_token,
|
reset_token,
|
||||||
trusted_apps,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1898,6 +1898,67 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
|
||||||
conn.pragma_update(None, "user_version", 43)?;
|
conn.pragma_update(None, "user_version", 43)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if current_version < 44 {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
DROP TABLE IF EXISTS trusted_apps;
|
||||||
|
CREATE TABLE tauth_owner_apps (
|
||||||
|
app_id TEXT PRIMARY KEY,
|
||||||
|
owner_user_id INTEGER NOT NULL,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
owner_certificate TEXT NOT NULL,
|
||||||
|
connection_mode TEXT NOT NULL CHECK (connection_mode IN ('hosted', 'forced_omikron')),
|
||||||
|
endpoint_url TEXT NOT NULL,
|
||||||
|
omikron_public_key TEXT,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE tauth_grants (
|
||||||
|
local_user_id INTEGER NOT NULL,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
app_name TEXT NOT NULL,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
scopes TEXT NOT NULL,
|
||||||
|
app_public_key TEXT NOT NULL,
|
||||||
|
manifest_hash TEXT NOT NULL,
|
||||||
|
security_state TEXT NOT NULL CHECK (security_state IN ('secure', 'insecure')),
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (local_user_id, app_id)
|
||||||
|
);
|
||||||
|
CREATE TABLE tauth_codes (
|
||||||
|
code_hash BLOB PRIMARY KEY,
|
||||||
|
local_user_id INTEGER NOT NULL,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
scopes TEXT NOT NULL,
|
||||||
|
pkce_challenge TEXT NOT NULL,
|
||||||
|
authorization_request TEXT NOT NULL,
|
||||||
|
locator TEXT NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX tauth_codes_expiry ON tauth_codes (expires_at);
|
||||||
|
CREATE TABLE tauth_sessions (
|
||||||
|
token_hash BLOB PRIMARY KEY,
|
||||||
|
local_user_id INTEGER NOT NULL,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
scopes TEXT NOT NULL,
|
||||||
|
locator TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX tauth_sessions_grant ON tauth_sessions (local_user_id, app_id);
|
||||||
|
CREATE TABLE tauth_metadata (
|
||||||
|
local_user_id INTEGER NOT NULL,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
json TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (local_user_id, app_id)
|
||||||
|
);
|
||||||
|
PRAGMA user_version = 44;
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1972,7 +2033,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, 43);
|
assert_eq!(version, 44);
|
||||||
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")?;
|
||||||
|
|
@ -1991,7 +2052,7 @@ 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, 43);
|
assert_eq!(version, 44);
|
||||||
for table in [
|
for table in [
|
||||||
"sync_heads",
|
"sync_heads",
|
||||||
"sync_events",
|
"sync_events",
|
||||||
|
|
@ -2072,7 +2133,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, 43);
|
assert_eq!(version, 44);
|
||||||
for column in [
|
for column in [
|
||||||
"id",
|
"id",
|
||||||
"user_id",
|
"user_id",
|
||||||
|
|
@ -2167,7 +2228,7 @@ mod tests {
|
||||||
)?;
|
)?;
|
||||||
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!(preserved, "remote_committed");
|
assert_eq!(preserved, "remote_committed");
|
||||||
assert_eq!(version, 43);
|
assert_eq!(version, 44);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2205,7 +2266,7 @@ mod tests {
|
||||||
})?;
|
})?;
|
||||||
assert_eq!(count, 0);
|
assert_eq!(count, 0);
|
||||||
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, 43);
|
assert_eq!(version, 44);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ walkdir = "2.5.0"
|
||||||
zip = "6.0.0"
|
zip = "6.0.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
hex = "*"
|
hex = "*"
|
||||||
|
sha2 = "0.10"
|
||||||
|
url = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,35 @@ fn credential_filename(username: &str) -> io::Result<String> {
|
||||||
Ok(format!("{username}.tu"))
|
Ok(format!("{username}.tu"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tauth_credential_path(app_id: &str) -> io::Result<PathBuf> {
|
||||||
|
if app_id.len() != 64 || !app_id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"invalid TAuth app ID",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(storage_directory()
|
||||||
|
.join("credentials")
|
||||||
|
.join("apps")
|
||||||
|
.join(format!("{}.ta", app_id.to_ascii_lowercase())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_tauth_credential(app_id: &str) -> io::Result<Option<Vec<u8>>> {
|
||||||
|
match fs::read(tauth_credential_path(app_id)?) {
|
||||||
|
Ok(value) => Ok(Some(value)),
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_tauth_credential(app_id: &str) -> io::Result<()> {
|
||||||
|
match fs::remove_file(tauth_credential_path(app_id)?) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn credential_path(username: &str) -> io::Result<PathBuf> {
|
pub fn credential_path(username: &str) -> io::Result<PathBuf> {
|
||||||
credential_path_in(&storage_directory(), username)
|
credential_path_in(&storage_directory(), username)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,5 @@ pub mod crypto_util;
|
||||||
pub mod file_util;
|
pub mod file_util;
|
||||||
pub mod mtp_compat;
|
pub mod mtp_compat;
|
||||||
pub mod route_target;
|
pub mod route_target;
|
||||||
|
pub mod ta;
|
||||||
pub mod tu;
|
pub mod tu;
|
||||||
|
|
|
||||||
275
iota-util/src/ta.rs
Normal file
275
iota-util/src/ta.rs
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
use crate::atomic_file;
|
||||||
|
use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
const MAGIC: &[u8; 4] = b"TAUT";
|
||||||
|
const VERSION: u16 = 1;
|
||||||
|
const MAX_FIELD_SIZE: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ConnectionMode {
|
||||||
|
Hosted {
|
||||||
|
omega_url: Url,
|
||||||
|
owner_iota_id: u64,
|
||||||
|
},
|
||||||
|
ForcedOmikron {
|
||||||
|
omikron_url: Url,
|
||||||
|
omikron_public_key: PublicKeyBundle,
|
||||||
|
owner_iota_id: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TaCredential {
|
||||||
|
pub keyring: Keyring,
|
||||||
|
pub owner_certificate: Vec<u8>,
|
||||||
|
pub mode: ConnectionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TaCredential {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("TaCredential")
|
||||||
|
.field("app_id", &self.app_id())
|
||||||
|
.field("owner_certificate", &"<signed certificate>")
|
||||||
|
.field("mode", &self.mode)
|
||||||
|
.field("keyring", &"<redacted>")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TaError {
|
||||||
|
InvalidFormat(&'static str),
|
||||||
|
InvalidKeyring,
|
||||||
|
InvalidPublicKey,
|
||||||
|
InvalidUrl,
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TaError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InvalidFormat(message) => write!(f, "invalid .ta credential: {message}"),
|
||||||
|
Self::InvalidKeyring => f.write_str("invalid .ta MTP keyring"),
|
||||||
|
Self::InvalidPublicKey => f.write_str("invalid forced Omikron public key"),
|
||||||
|
Self::InvalidUrl => f.write_str("invalid .ta connection URL"),
|
||||||
|
Self::Io(error) => error.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TaError {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for TaError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::Io(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaCredential {
|
||||||
|
pub fn app_id(&self) -> String {
|
||||||
|
app_id(&self.keyring.public_key_bundle())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn owner_iota_id(&self) -> u64 {
|
||||||
|
match &self.mode {
|
||||||
|
ConnectionMode::Hosted { owner_iota_id, .. }
|
||||||
|
| ConnectionMode::ForcedOmikron { owner_iota_id, .. } => *owner_iota_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes(&self) -> Result<Vec<u8>, TaError> {
|
||||||
|
let keyring = self
|
||||||
|
.keyring
|
||||||
|
.try_to_bytes()
|
||||||
|
.map_err(|_| TaError::InvalidKeyring)?;
|
||||||
|
let mut output = Vec::new();
|
||||||
|
output.extend_from_slice(MAGIC);
|
||||||
|
output.extend_from_slice(&VERSION.to_be_bytes());
|
||||||
|
push_bytes(&mut output, &keyring)?;
|
||||||
|
push_bytes(&mut output, &self.owner_certificate)?;
|
||||||
|
match &self.mode {
|
||||||
|
ConnectionMode::Hosted {
|
||||||
|
omega_url,
|
||||||
|
owner_iota_id,
|
||||||
|
} => {
|
||||||
|
output.push(0);
|
||||||
|
output.extend_from_slice(&owner_iota_id.to_be_bytes());
|
||||||
|
push_bytes(&mut output, omega_url.as_str().as_bytes())?;
|
||||||
|
}
|
||||||
|
ConnectionMode::ForcedOmikron {
|
||||||
|
omikron_url,
|
||||||
|
omikron_public_key,
|
||||||
|
owner_iota_id,
|
||||||
|
} => {
|
||||||
|
output.push(1);
|
||||||
|
output.extend_from_slice(&owner_iota_id.to_be_bytes());
|
||||||
|
push_bytes(&mut output, omikron_url.as_str().as_bytes())?;
|
||||||
|
let public_key = omikron_public_key
|
||||||
|
.try_as_bytes()
|
||||||
|
.map_err(|_| TaError::InvalidPublicKey)?;
|
||||||
|
push_bytes(&mut output, &public_key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(input: &[u8]) -> Result<Self, TaError> {
|
||||||
|
let mut reader = Reader::new(input);
|
||||||
|
if reader.take(4)? != MAGIC {
|
||||||
|
return Err(TaError::InvalidFormat("bad magic"));
|
||||||
|
}
|
||||||
|
let version = u16::from_be_bytes(reader.take(2)?.try_into().unwrap());
|
||||||
|
if version != VERSION {
|
||||||
|
return Err(TaError::InvalidFormat("unsupported version"));
|
||||||
|
}
|
||||||
|
let keyring = Keyring::from_bytes(reader.bytes()?).map_err(|_| TaError::InvalidKeyring)?;
|
||||||
|
let owner_certificate = reader.bytes()?.to_vec();
|
||||||
|
if owner_certificate.is_empty() {
|
||||||
|
return Err(TaError::InvalidFormat("missing owner certificate"));
|
||||||
|
}
|
||||||
|
let mode = *reader
|
||||||
|
.take(1)?
|
||||||
|
.first()
|
||||||
|
.ok_or(TaError::InvalidFormat("missing connection mode"))?;
|
||||||
|
let owner_iota_id = u64::from_be_bytes(reader.take(8)?.try_into().unwrap());
|
||||||
|
if owner_iota_id == 0 {
|
||||||
|
return Err(TaError::InvalidFormat("invalid owner Iota ID"));
|
||||||
|
}
|
||||||
|
let endpoint = parse_https_url(reader.bytes()?)?;
|
||||||
|
let mode = match mode {
|
||||||
|
0 => ConnectionMode::Hosted {
|
||||||
|
omega_url: endpoint,
|
||||||
|
owner_iota_id,
|
||||||
|
},
|
||||||
|
1 => ConnectionMode::ForcedOmikron {
|
||||||
|
omikron_url: endpoint,
|
||||||
|
omikron_public_key: PublicKeyBundle::from_bytes(reader.bytes()?)
|
||||||
|
.map_err(|_| TaError::InvalidPublicKey)?,
|
||||||
|
owner_iota_id,
|
||||||
|
},
|
||||||
|
_ => return Err(TaError::InvalidFormat("unknown connection mode")),
|
||||||
|
};
|
||||||
|
if !reader.remaining().is_empty() {
|
||||||
|
return Err(TaError::InvalidFormat("trailing bytes"));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
keyring,
|
||||||
|
owner_certificate,
|
||||||
|
mode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_internal(&self, path: &Path) -> Result<(), TaError> {
|
||||||
|
atomic_file::replace_private(path, &self.to_bytes()?, 0)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export(&self, path: &Path) -> Result<(), TaError> {
|
||||||
|
atomic_file::create_private(path, &self.to_bytes()?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_id(public_key: &PublicKeyBundle) -> String {
|
||||||
|
let bytes = public_key
|
||||||
|
.try_as_bytes()
|
||||||
|
.expect("validated public key bundle must serialize");
|
||||||
|
hex::encode(Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_https_url(bytes: &[u8]) -> Result<Url, TaError> {
|
||||||
|
let value = std::str::from_utf8(bytes).map_err(|_| TaError::InvalidUrl)?;
|
||||||
|
let url = Url::parse(value).map_err(|_| TaError::InvalidUrl)?;
|
||||||
|
if url.scheme() != "https" || url.host_str().is_none() {
|
||||||
|
return Err(TaError::InvalidUrl);
|
||||||
|
}
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_bytes(output: &mut Vec<u8>, bytes: &[u8]) -> Result<(), TaError> {
|
||||||
|
if bytes.len() > MAX_FIELD_SIZE {
|
||||||
|
return Err(TaError::InvalidFormat("field too large"));
|
||||||
|
}
|
||||||
|
output.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
||||||
|
output.extend_from_slice(bytes);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Reader<'a> {
|
||||||
|
remaining: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Reader<'a> {
|
||||||
|
fn new(input: &'a [u8]) -> Self {
|
||||||
|
Self { remaining: input }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&mut self, length: usize) -> Result<&'a [u8], TaError> {
|
||||||
|
if self.remaining.len() < length {
|
||||||
|
return Err(TaError::InvalidFormat("truncated field"));
|
||||||
|
}
|
||||||
|
let (value, remaining) = self.remaining.split_at(length);
|
||||||
|
self.remaining = remaining;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(&mut self) -> Result<&'a [u8], TaError> {
|
||||||
|
let length = u32::from_be_bytes(self.take(4)?.try_into().unwrap()) as usize;
|
||||||
|
if length > MAX_FIELD_SIZE {
|
||||||
|
return Err(TaError::InvalidFormat("field too large"));
|
||||||
|
}
|
||||||
|
self.take(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remaining(&self) -> &'a [u8] {
|
||||||
|
self.remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binary_credential_round_trip_preserves_identity() {
|
||||||
|
let credential = TaCredential {
|
||||||
|
keyring: Keyring::generate(),
|
||||||
|
owner_certificate: br#"{"owner":"iota:7"}"#.to_vec(),
|
||||||
|
mode: ConnectionMode::Hosted {
|
||||||
|
omega_url: Url::parse("https://omega.example").unwrap(),
|
||||||
|
owner_iota_id: 7,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let app_id = credential.app_id();
|
||||||
|
let parsed = TaCredential::from_bytes(&credential.to_bytes().unwrap()).unwrap();
|
||||||
|
assert_eq!(parsed.app_id(), app_id);
|
||||||
|
assert_eq!(parsed.owner_iota_id(), 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_refuses_overwrite_and_is_private() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let path = directory.path().join("app.ta");
|
||||||
|
let credential = TaCredential {
|
||||||
|
keyring: Keyring::generate(),
|
||||||
|
owner_certificate: b"certificate".to_vec(),
|
||||||
|
mode: ConnectionMode::Hosted {
|
||||||
|
omega_url: Url::parse("https://omega.example").unwrap(),
|
||||||
|
owner_iota_id: 7,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
credential.export(&path).unwrap();
|
||||||
|
assert!(credential.export(&path).is_err());
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
|
||||||
|
0o600
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.22"
|
||||||
iota-cli = { path = "../iota-cli" }
|
iota-cli = { path = "../iota-cli" }
|
||||||
iota-ipc = { path = "../iota-ipc" }
|
iota-ipc = { path = "../iota-ipc" }
|
||||||
iota-installer = { path = "../iota-installer" }
|
iota-installer = { path = "../iota-installer" }
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ enum CliCommand {
|
||||||
Status,
|
Status,
|
||||||
Tasks,
|
Tasks,
|
||||||
Users(UsersArgs),
|
Users(UsersArgs),
|
||||||
|
Apps(AppsArgs),
|
||||||
Omikron(OmikronArgs),
|
Omikron(OmikronArgs),
|
||||||
Identity(IdentityArgs),
|
Identity(IdentityArgs),
|
||||||
Daemon(DaemonArgs),
|
Daemon(DaemonArgs),
|
||||||
|
|
@ -110,6 +111,60 @@ struct UsersArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: UsersAction,
|
action: UsersAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
struct AppsArgs {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: AppsAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum AppsAction {
|
||||||
|
List,
|
||||||
|
Show {
|
||||||
|
app_id: String,
|
||||||
|
},
|
||||||
|
Create {
|
||||||
|
#[arg(long)]
|
||||||
|
owner_user_id: i64,
|
||||||
|
#[arg(long)]
|
||||||
|
domain: String,
|
||||||
|
#[arg(long)]
|
||||||
|
name: String,
|
||||||
|
#[arg(long = "redirect", required = true)]
|
||||||
|
redirects: Vec<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
output: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
manifest_output: PathBuf,
|
||||||
|
#[command(subcommand)]
|
||||||
|
connection: AppConnectionAction,
|
||||||
|
},
|
||||||
|
Export {
|
||||||
|
app_id: String,
|
||||||
|
#[arg(long)]
|
||||||
|
output: PathBuf,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
app_id: String,
|
||||||
|
#[arg(long)]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum AppConnectionAction {
|
||||||
|
Hosted {
|
||||||
|
#[arg(long)]
|
||||||
|
omega_url: String,
|
||||||
|
},
|
||||||
|
ForcedOmikron {
|
||||||
|
#[arg(long)]
|
||||||
|
omikron_url: String,
|
||||||
|
#[arg(long)]
|
||||||
|
omikron_public_key: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
enum UsersAction {
|
enum UsersAction {
|
||||||
List,
|
List,
|
||||||
|
|
@ -412,6 +467,27 @@ pub enum Command {
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
output: PathBuf,
|
output: PathBuf,
|
||||||
},
|
},
|
||||||
|
AppsList,
|
||||||
|
AppsShow {
|
||||||
|
app_id: String,
|
||||||
|
},
|
||||||
|
AppsCreate {
|
||||||
|
owner_user_id: i64,
|
||||||
|
domain: String,
|
||||||
|
name: String,
|
||||||
|
redirects: Vec<String>,
|
||||||
|
connection: iota_ipc::TAuthConnectionInput,
|
||||||
|
output: PathBuf,
|
||||||
|
manifest_output: PathBuf,
|
||||||
|
},
|
||||||
|
AppsExport {
|
||||||
|
app_id: String,
|
||||||
|
output: PathBuf,
|
||||||
|
},
|
||||||
|
AppsDelete {
|
||||||
|
app_id: String,
|
||||||
|
confirmed: bool,
|
||||||
|
},
|
||||||
OmikronReconnect,
|
OmikronReconnect,
|
||||||
IdentityRotate {
|
IdentityRotate {
|
||||||
confirmed: bool,
|
confirmed: bool,
|
||||||
|
|
@ -577,6 +653,43 @@ impl CliInvocation {
|
||||||
action: UserCredentialAction::Export { user_id, output },
|
action: UserCredentialAction::Export { user_id, output },
|
||||||
} => Command::UsersExportCredential { user_id, output },
|
} => Command::UsersExportCredential { user_id, output },
|
||||||
},
|
},
|
||||||
|
Some(CliCommand::Apps(apps)) => match apps.action {
|
||||||
|
AppsAction::List => Command::AppsList,
|
||||||
|
AppsAction::Show { app_id } => Command::AppsShow { app_id },
|
||||||
|
AppsAction::Create {
|
||||||
|
owner_user_id,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirects,
|
||||||
|
output,
|
||||||
|
manifest_output,
|
||||||
|
connection,
|
||||||
|
} => Command::AppsCreate {
|
||||||
|
owner_user_id,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirects,
|
||||||
|
connection: match connection {
|
||||||
|
AppConnectionAction::Hosted { omega_url } => {
|
||||||
|
iota_ipc::TAuthConnectionInput::Hosted { omega_url }
|
||||||
|
}
|
||||||
|
AppConnectionAction::ForcedOmikron {
|
||||||
|
omikron_url,
|
||||||
|
omikron_public_key,
|
||||||
|
} => iota_ipc::TAuthConnectionInput::ForcedOmikron {
|
||||||
|
omikron_url,
|
||||||
|
omikron_public_key,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output,
|
||||||
|
manifest_output,
|
||||||
|
},
|
||||||
|
AppsAction::Export { app_id, output } => Command::AppsExport { app_id, output },
|
||||||
|
AppsAction::Delete { app_id, yes } => Command::AppsDelete {
|
||||||
|
app_id,
|
||||||
|
confirmed: resolve_confirmed(yes),
|
||||||
|
},
|
||||||
|
},
|
||||||
Some(CliCommand::Omikron(omikron)) => match omikron.action {
|
Some(CliCommand::Omikron(omikron)) => match omikron.action {
|
||||||
OmikronAction::Reconnect => Command::OmikronReconnect,
|
OmikronAction::Reconnect => Command::OmikronReconnect,
|
||||||
OmikronAction::Status => Command::OmikronStatus,
|
OmikronAction::Status => Command::OmikronStatus,
|
||||||
|
|
|
||||||
189
iota/src/main.rs
189
iota/src/main.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use iota_cli::{
|
use iota_cli::{
|
||||||
ipc_client::IpcClient, screens::main_screen::MainScreen, theme,
|
ipc_client::IpcClient, screens::main_screen::MainScreen, theme,
|
||||||
ui::start_bootstrap_tui_with_config,
|
ui::start_bootstrap_tui_with_config,
|
||||||
|
|
@ -388,8 +389,13 @@ fn print_help() {
|
||||||
println!(" users repair force-detach <ID> Remove local management (requires --yes)");
|
println!(" users repair force-detach <ID> Remove local management (requires --yes)");
|
||||||
println!(" users forget <ID> Forget an empty released residency (requires --yes)");
|
println!(" users forget <ID> Forget an empty released residency (requires --yes)");
|
||||||
println!(" users apps revoke <ID> <APP> Revoke a trusted app (requires --yes)");
|
println!(" users apps revoke <ID> <APP> Revoke a trusted app (requires --yes)");
|
||||||
println!(" users apps revoke-all <ID> Revoke all trusted apps (requires --yes)");
|
println!(" users apps revoke-all <ID> Revoke all TAuth grants (requires --yes)");
|
||||||
println!(" users credential export <ID> --output <PATH> Export the local TU");
|
println!(" users credential export <ID> --output <PATH> Export the local TU");
|
||||||
|
println!(" apps list List owned TAuth apps");
|
||||||
|
println!(" apps show <APP_ID> Show an owned TAuth app");
|
||||||
|
println!(" apps create ... Create and export a TAuth app");
|
||||||
|
println!(" apps export <APP_ID> --output <PATH> Export an existing .ta");
|
||||||
|
println!(" apps delete <APP_ID> Delete app key and registration (requires --yes)");
|
||||||
println!(" omikron status Show Omikron connection status");
|
println!(" omikron status Show Omikron connection status");
|
||||||
println!(" omikron reconnect Reconnect to Omikron");
|
println!(" omikron reconnect Reconnect to Omikron");
|
||||||
println!(" identity rotate Rotate identity keys (requires --yes)");
|
println!(" identity rotate Rotate identity keys (requires --yes)");
|
||||||
|
|
@ -536,6 +542,8 @@ async fn run_command(
|
||||||
) -> Result<(), StartupError> {
|
) -> Result<(), StartupError> {
|
||||||
let color = ColorConfig::new();
|
let color = ColorConfig::new();
|
||||||
let mut credential_output = None;
|
let mut credential_output = None;
|
||||||
|
let mut tauth_credential_output = None;
|
||||||
|
let mut tauth_manifest_output = None;
|
||||||
let request = match command {
|
let request = match command {
|
||||||
Command::Status => LocalRequest::GetStatus,
|
Command::Status => LocalRequest::GetStatus,
|
||||||
Command::Tasks => LocalRequest::ListTasks,
|
Command::Tasks => LocalRequest::ListTasks,
|
||||||
|
|
@ -623,11 +631,11 @@ async fn run_command(
|
||||||
user_id,
|
user_id,
|
||||||
app_id,
|
app_id,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
} => LocalRequest::RevokeTrustedApp { user_id, app_id },
|
} => LocalRequest::RevokeTAuthGrant { user_id, app_id },
|
||||||
Command::UsersRevokeAllApps {
|
Command::UsersRevokeAllApps {
|
||||||
user_id,
|
user_id,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
} => LocalRequest::RevokeAllTrustedApps { user_id },
|
} => LocalRequest::RevokeAllTAuthGrants { user_id },
|
||||||
Command::UsersExportCredential { user_id, output } => {
|
Command::UsersExportCredential { user_id, output } => {
|
||||||
let daemon_status = ipc.daemon_status();
|
let daemon_status = ipc.daemon_status();
|
||||||
if !daemon_status
|
if !daemon_status
|
||||||
|
|
@ -643,6 +651,54 @@ async fn run_command(
|
||||||
credential_output = Some(output);
|
credential_output = Some(output);
|
||||||
LocalRequest::ExportUserCredential { user_id }
|
LocalRequest::ExportUserCredential { user_id }
|
||||||
}
|
}
|
||||||
|
Command::AppsList => LocalRequest::ListTAuthApps,
|
||||||
|
Command::AppsShow { app_id } => LocalRequest::GetTAuthApp { app_id },
|
||||||
|
Command::AppsCreate {
|
||||||
|
owner_user_id,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirects,
|
||||||
|
connection,
|
||||||
|
output,
|
||||||
|
manifest_output,
|
||||||
|
} => {
|
||||||
|
if output == manifest_output {
|
||||||
|
return Err(StartupError::InvalidCommand(
|
||||||
|
".ta and manifest outputs must use different paths.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for path in [&output, &manifest_output] {
|
||||||
|
if path.exists() {
|
||||||
|
return Err(StartupError::InvalidCommand(format!(
|
||||||
|
"Refusing to overwrite {}.",
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tauth_credential_output = Some(output);
|
||||||
|
tauth_manifest_output = Some(manifest_output);
|
||||||
|
LocalRequest::CreateTAuthApp {
|
||||||
|
owner_user_id,
|
||||||
|
domain,
|
||||||
|
name,
|
||||||
|
redirects,
|
||||||
|
connection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::AppsExport { app_id, output } => {
|
||||||
|
if output.exists() {
|
||||||
|
return Err(StartupError::InvalidCommand(format!(
|
||||||
|
"Refusing to overwrite {}.",
|
||||||
|
output.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
tauth_credential_output = Some(output);
|
||||||
|
LocalRequest::ExportTAuthApp { app_id }
|
||||||
|
}
|
||||||
|
Command::AppsDelete {
|
||||||
|
app_id,
|
||||||
|
confirmed: true,
|
||||||
|
} => LocalRequest::DeleteTAuthApp { app_id },
|
||||||
Command::OmikronReconnect => LocalRequest::ReconnectOmikron,
|
Command::OmikronReconnect => LocalRequest::ReconnectOmikron,
|
||||||
Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity,
|
Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity,
|
||||||
Command::RegenerateKeys { confirmed: true } => LocalRequest::RotateIotaIdentity,
|
Command::RegenerateKeys { confirmed: true } => LocalRequest::RotateIotaIdentity,
|
||||||
|
|
@ -683,6 +739,9 @@ async fn run_command(
|
||||||
| Command::UsersRevokeAllApps {
|
| Command::UsersRevokeAllApps {
|
||||||
confirmed: false, ..
|
confirmed: false, ..
|
||||||
}
|
}
|
||||||
|
| Command::AppsDelete {
|
||||||
|
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 }
|
||||||
|
|
@ -745,7 +804,63 @@ async fn run_command(
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if credential_output.is_some() {
|
if let ResponsePayload::TAuthAppCreated {
|
||||||
|
app,
|
||||||
|
credential,
|
||||||
|
txt_record,
|
||||||
|
manifest_template,
|
||||||
|
} = &payload
|
||||||
|
{
|
||||||
|
let credential_path = tauth_credential_output.as_deref().ok_or_else(|| {
|
||||||
|
StartupError::Other("Missing TAuth credential export destination.".into())
|
||||||
|
})?;
|
||||||
|
let manifest_path = tauth_manifest_output.as_deref().ok_or_else(|| {
|
||||||
|
StartupError::Other("Missing TAuth manifest export destination.".into())
|
||||||
|
})?;
|
||||||
|
let bytes = STANDARD.decode(&credential.0).map_err(|error| {
|
||||||
|
StartupError::Other(format!("Daemon returned invalid .ta data: {error}"))
|
||||||
|
})?;
|
||||||
|
iota_util::atomic_file::create_private(credential_path, &bytes).map_err(
|
||||||
|
|error| {
|
||||||
|
StartupError::Other(format!(
|
||||||
|
"Cannot export credential to {}: {error}",
|
||||||
|
credential_path.display()
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
if let Err(error) = iota_util::atomic_file::create_private(
|
||||||
|
manifest_path,
|
||||||
|
manifest_template.as_bytes(),
|
||||||
|
) {
|
||||||
|
let _ = std::fs::remove_file(credential_path);
|
||||||
|
return Err(StartupError::Other(format!(
|
||||||
|
"Cannot export manifest template to {}: {error}",
|
||||||
|
manifest_path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
println!("Created TAuth app {} for {}.", app.app_id, app.domain);
|
||||||
|
println!(".ta: {}", credential_path.display());
|
||||||
|
println!("TXT _tauth.{}: {}", app.domain, txt_record);
|
||||||
|
println!("Manifest: {}", manifest_path.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let ResponsePayload::TAuthAppCredentialExport { app_id, credential } = &payload {
|
||||||
|
let path = tauth_credential_output.as_deref().ok_or_else(|| {
|
||||||
|
StartupError::Other("Missing TAuth credential export destination.".into())
|
||||||
|
})?;
|
||||||
|
let bytes = STANDARD.decode(&credential.0).map_err(|error| {
|
||||||
|
StartupError::Other(format!("Daemon returned invalid .ta data: {error}"))
|
||||||
|
})?;
|
||||||
|
iota_util::atomic_file::create_private(path, &bytes).map_err(|error| {
|
||||||
|
StartupError::Other(format!(
|
||||||
|
"Cannot export credential to {}: {error}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
println!("Exported TAuth app {app_id} to {}.", path.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if credential_output.is_some() || tauth_credential_output.is_some() {
|
||||||
return Err(StartupError::Other(
|
return Err(StartupError::Other(
|
||||||
"The daemon returned an unexpected credential export response.".into(),
|
"The daemon returned an unexpected credential export response.".into(),
|
||||||
));
|
));
|
||||||
|
|
@ -822,6 +937,28 @@ async fn run_command(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResponsePayload::TAuthApps(apps) => {
|
||||||
|
if apps.is_empty() {
|
||||||
|
println!("{}", cli_color::muted(&color, "No TAuth apps."));
|
||||||
|
} else {
|
||||||
|
for app in apps {
|
||||||
|
println!("{} {} {}", app.app_id, app.domain, app.connection_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResponsePayload::TAuthApp(app) => {
|
||||||
|
println!("App ID: {}", app.app_id);
|
||||||
|
println!("Domain: {}", app.domain);
|
||||||
|
println!("Owner user: {}", app.owner_user_id);
|
||||||
|
println!("Mode: {}", app.connection_mode);
|
||||||
|
println!("Endpoint: {}", app.endpoint_url);
|
||||||
|
}
|
||||||
|
ResponsePayload::TAuthAppCreated { .. }
|
||||||
|
| ResponsePayload::TAuthAppCredentialExport { .. } => {
|
||||||
|
return Err(StartupError::Other(
|
||||||
|
"Refusing to display TAuth credential material.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
ResponsePayload::InvitationCreated(invitation) => {
|
ResponsePayload::InvitationCreated(invitation) => {
|
||||||
println!("Invitation: {}", invitation.invitation_id);
|
println!("Invitation: {}", invitation.invitation_id);
|
||||||
println!("Token: {}", invitation.raw_token.0);
|
println!("Token: {}", invitation.raw_token.0);
|
||||||
|
|
@ -928,8 +1065,8 @@ async fn run_command(
|
||||||
println!("Display Name: {name}");
|
println!("Display Name: {name}");
|
||||||
}
|
}
|
||||||
println!("Created At: {}", user.created_at);
|
println!("Created At: {}", user.created_at);
|
||||||
if !user.trusted_apps.is_empty() {
|
if !user.tauth_grants.is_empty() {
|
||||||
println!("Trusted Apps: {}", user.trusted_apps.join(", "));
|
println!("TAuth grants: {}", user.tauth_grants.join(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResponsePayload::TuCredentialPreview(preview) => {
|
ResponsePayload::TuCredentialPreview(preview) => {
|
||||||
|
|
@ -950,7 +1087,7 @@ async fn run_command(
|
||||||
println!("Local state: {:?}", diagnostics.local_state);
|
println!("Local state: {:?}", diagnostics.local_state);
|
||||||
println!("Hosted data: {}", diagnostics.data_present);
|
println!("Hosted data: {}", diagnostics.data_present);
|
||||||
println!("Credential: {:?}", diagnostics.credential_status);
|
println!("Credential: {:?}", diagnostics.credential_status);
|
||||||
println!("Trusted applications: {}", diagnostics.trusted_app_count);
|
println!("TAuth grants: {}", diagnostics.tauth_grant_count);
|
||||||
if let Some(operation) = &diagnostics.pending_operation {
|
if let Some(operation) = &diagnostics.pending_operation {
|
||||||
println!("Pending operation: {operation}");
|
println!("Pending operation: {operation}");
|
||||||
}
|
}
|
||||||
|
|
@ -1005,7 +1142,12 @@ async fn run_command(
|
||||||
/// Text remains an operator-oriented presentation; JSON and YAML must never
|
/// Text remains an operator-oriented presentation; JSON and YAML must never
|
||||||
/// require consumers to parse it.
|
/// require consumers to parse it.
|
||||||
fn render_structured(payload: &ResponsePayload, output: OutputFormat) -> Result<(), StartupError> {
|
fn render_structured(payload: &ResponsePayload, output: OutputFormat) -> Result<(), StartupError> {
|
||||||
if matches!(payload, ResponsePayload::UserCredentialExport { .. }) {
|
if matches!(
|
||||||
|
payload,
|
||||||
|
ResponsePayload::UserCredentialExport { .. }
|
||||||
|
| ResponsePayload::TAuthAppCreated { .. }
|
||||||
|
| ResponsePayload::TAuthAppCredentialExport { .. }
|
||||||
|
) {
|
||||||
return Err(StartupError::Other(
|
return Err(StartupError::Other(
|
||||||
"Refusing to encode credential material as ordinary output.".into(),
|
"Refusing to encode credential material as ordinary output.".into(),
|
||||||
));
|
));
|
||||||
|
|
@ -1096,6 +1238,31 @@ fn render_table(payload: &ResponsePayload) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResponsePayload::TAuthApps(apps) => {
|
||||||
|
if apps.is_empty() {
|
||||||
|
println!("No TAuth apps.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("{:<66} {:<28} MODE", "APP ID", "DOMAIN");
|
||||||
|
for app in apps {
|
||||||
|
println!(
|
||||||
|
"{:<66} {:<28} {}",
|
||||||
|
app.app_id, app.domain, app.connection_mode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResponsePayload::TAuthApp(app) => {
|
||||||
|
println!("{:<18} {}", "App ID", app.app_id);
|
||||||
|
println!("{:<18} {}", "Domain", app.domain);
|
||||||
|
println!("{:<18} {}", "Owner user", app.owner_user_id);
|
||||||
|
println!("{:<18} {}", "Public key", app.public_key);
|
||||||
|
println!("{:<18} {}", "Mode", app.connection_mode);
|
||||||
|
println!("{:<18} {}", "Endpoint", app.endpoint_url);
|
||||||
|
}
|
||||||
|
ResponsePayload::TAuthAppCreated { .. }
|
||||||
|
| ResponsePayload::TAuthAppCredentialExport { .. } => {
|
||||||
|
println!("TAuth credential material withheld.");
|
||||||
|
}
|
||||||
ResponsePayload::Tasks(tasks) => {
|
ResponsePayload::Tasks(tasks) => {
|
||||||
if tasks.is_empty() {
|
if tasks.is_empty() {
|
||||||
println!("No active tasks.");
|
println!("No active tasks.");
|
||||||
|
|
@ -1216,7 +1383,7 @@ fn render_table(payload: &ResponsePayload) {
|
||||||
println!("{:<15} {:?}", "Local state", diagnostics.local_state);
|
println!("{:<15} {:?}", "Local state", diagnostics.local_state);
|
||||||
println!("{:<15} {}", "Hosted data", diagnostics.data_present);
|
println!("{:<15} {}", "Hosted data", diagnostics.data_present);
|
||||||
println!("{:<15} {:?}", "Credential", diagnostics.credential_status);
|
println!("{:<15} {:?}", "Credential", diagnostics.credential_status);
|
||||||
println!("{:<15} {}", "Trusted apps", diagnostics.trusted_app_count);
|
println!("{:<15} {}", "TAuth grants", diagnostics.tauth_grant_count);
|
||||||
}
|
}
|
||||||
ResponsePayload::UserCredentialExport { .. } => {
|
ResponsePayload::UserCredentialExport { .. } => {
|
||||||
println!("Credential export payload withheld.");
|
println!("Credential export payload withheld.");
|
||||||
|
|
@ -1239,8 +1406,8 @@ fn render_table(payload: &ResponsePayload) {
|
||||||
println!("{:<15} {}", "Display Name", name);
|
println!("{:<15} {}", "Display Name", name);
|
||||||
}
|
}
|
||||||
println!("{:<15} {}", "Created At", user.created_at);
|
println!("{:<15} {}", "Created At", user.created_at);
|
||||||
if !user.trusted_apps.is_empty() {
|
if !user.tauth_grants.is_empty() {
|
||||||
println!("{:<15} {}", "Trusted Apps", user.trusted_apps.join(", "));
|
println!("{:<15} {}", "TAuth grants", user.tauth_grants.join(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 91d03974632a2897de5457d52170dda9d9e36c3d
|
Subproject commit 10e418ca69c27db0e89ac7dc6a2a348ad9b70017
|
||||||
|
|
@ -20,10 +20,17 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "1f19a0d897c2
|
||||||
] }
|
] }
|
||||||
|
|
||||||
dashmap = "6.2.1"
|
dashmap = "6.2.1"
|
||||||
|
hex = "0.4"
|
||||||
json = "*"
|
json = "*"
|
||||||
reqwest = "0.13.2"
|
reqwest = "0.13.2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
|
thiserror = "2"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
tokio-util = { version = "0.7", features = ["rt"] }
|
||||||
uuid = { version = "*", features = ["v4"] }
|
uuid = { version = "*", features = ["v4"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
||||||
|
trust-dns-resolver = { version = "0.23", features = ["tokio-runtime"] }
|
||||||
|
url = "2"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod identity;
|
||||||
pub mod omega_discovery;
|
pub mod omega_discovery;
|
||||||
pub mod omikron_connection;
|
pub mod omikron_connection;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
|
pub mod tauth;
|
||||||
pub mod user_ops;
|
pub mod user_ops;
|
||||||
|
|
||||||
pub use client::{OmikronClient, OmikronError, OmikronStartupError};
|
pub use client::{OmikronClient, OmikronError, OmikronStartupError};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use dashmap::{DashMap, DashSet};
|
use dashmap::{DashMap, DashSet};
|
||||||
use iota_logger::{log, log_cv_in, log_cv_out, log_t};
|
use iota_logger::{log, log_cv_in, log_cv_out, log_t};
|
||||||
use iota_state::AppState;
|
use iota_state::AppState;
|
||||||
|
|
@ -5,11 +6,11 @@ use iota_storage::util::config_util::{CONFIG, modify_config};
|
||||||
use iota_storage::util::relay_replay;
|
use iota_storage::util::relay_replay;
|
||||||
use iota_storage::util::{chat_files, client_relay_delivery, relay_queue};
|
use iota_storage::util::{chat_files, client_relay_delivery, relay_queue};
|
||||||
use iota_util::crypto_helper::{self, keyring_from_base64};
|
use iota_util::crypto_helper::{self, keyring_from_base64};
|
||||||
use iota_util::crypto_util::{self};
|
|
||||||
use mtp::client::{Client, ClientConfig, MTPConnection, Policy, SendMode, Sender};
|
use mtp::client::{Client, ClientConfig, MTPConnection, Policy, SendMode, Sender};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||||
use mtp::crypto::{Keyring, PublicKeyBundle};
|
use mtp::crypto::{Ed25519Signer, Keyring, PublicKeyBundle, SignatureScheme};
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
|
use serde::Serialize;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
@ -30,6 +31,7 @@ use iota_connection::relay::{
|
||||||
RelayValidationError, forward_verified_relay, open_verified_relay_content,
|
RelayValidationError, forward_verified_relay, open_verified_relay_content,
|
||||||
verify_relay_metadata,
|
verify_relay_metadata,
|
||||||
};
|
};
|
||||||
|
use iota_identity::AuthorityLocator;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Configuration
|
// Configuration
|
||||||
|
|
@ -345,17 +347,31 @@ pub enum ConnectionState {
|
||||||
Connected { identified: bool },
|
Connected { identified: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Serialize)]
|
||||||
struct PendingAppChallenge {
|
struct TAuthLocatorUnsigned<'a> {
|
||||||
challenge: String,
|
version: u16,
|
||||||
user_id: i64,
|
principal: &'a str,
|
||||||
app_identifier: String,
|
omikron_url: &'a str,
|
||||||
|
omikron_public_key: &'a str,
|
||||||
|
target_iota_id: u64,
|
||||||
|
iota_public_key: &'a str,
|
||||||
|
issued_at: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Serialize)]
|
||||||
struct AppSession {
|
struct TAuthLocator<'a> {
|
||||||
connection_id: Uuid,
|
version: u16,
|
||||||
app_identifier: String,
|
principal: &'a str,
|
||||||
|
omikron_url: &'a str,
|
||||||
|
omikron_public_key: &'a str,
|
||||||
|
target_iota_id: u64,
|
||||||
|
iota_public_key: &'a str,
|
||||||
|
issued_at: i64,
|
||||||
|
signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_array(values: &[String]) -> DataValue {
|
||||||
|
DataValue::Array(values.iter().cloned().map(DataValue::Str).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionState {
|
impl ConnectionState {
|
||||||
|
|
@ -385,8 +401,7 @@ pub struct OmikronConnection {
|
||||||
reconnect_on_close: Arc<RwLock<bool>>,
|
reconnect_on_close: Arc<RwLock<bool>>,
|
||||||
auth_failure: Arc<RwLock<Option<String>>>,
|
auth_failure: Arc<RwLock<Option<String>>>,
|
||||||
keyring: Arc<RwLock<Option<Arc<Keyring>>>>,
|
keyring: Arc<RwLock<Option<Arc<Keyring>>>>,
|
||||||
app_challenges: Arc<DashMap<u64, PendingAppChallenge>>,
|
http_client: reqwest::Client,
|
||||||
app_sessions: Arc<DashMap<u64, AppSession>>,
|
|
||||||
session_manager: Arc<iota_auth::SessionManager>,
|
session_manager: Arc<iota_auth::SessionManager>,
|
||||||
handler_semaphore: Arc<Semaphore>,
|
handler_semaphore: Arc<Semaphore>,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
|
|
@ -421,8 +436,7 @@ impl OmikronConnection {
|
||||||
reconnect_on_close: Arc::new(RwLock::new(true)),
|
reconnect_on_close: Arc::new(RwLock::new(true)),
|
||||||
auth_failure: Arc::new(RwLock::new(None)),
|
auth_failure: Arc::new(RwLock::new(None)),
|
||||||
keyring: Arc::new(RwLock::new(None)),
|
keyring: Arc::new(RwLock::new(None)),
|
||||||
app_challenges: Arc::new(DashMap::new()),
|
http_client: reqwest::Client::new(),
|
||||||
app_sessions: Arc::new(DashMap::new()),
|
|
||||||
session_manager: Arc::new(iota_auth::SessionManager::default()),
|
session_manager: Arc::new(iota_auth::SessionManager::default()),
|
||||||
handler_semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)),
|
handler_semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)),
|
||||||
cancellation,
|
cancellation,
|
||||||
|
|
@ -1676,12 +1690,18 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch!(GetChatSecret, handle_get_chat_secret);
|
dispatch!(GetChatSecret, handle_get_chat_secret);
|
||||||
dispatch!(AppIdentification, handle_app_identification);
|
dispatch!(TAuthAuthorize, handle_tauth_authorize);
|
||||||
dispatch!(AppChallengeResponse, handle_app_challenge_response);
|
dispatch!(TAuthExchangeCode, handle_tauth_exchange_code);
|
||||||
dispatch!(SaveAppData, handle_save_app_data);
|
dispatch!(TAuthUser, handle_tauth_user);
|
||||||
dispatch!(LoadAppData, handle_load_app_data);
|
dispatch!(TAuthContacts, handle_tauth_contacts);
|
||||||
dispatch!(CreateApp, handle_create_app);
|
dispatch!(TAuthPublicLookup, handle_tauth_public_lookup);
|
||||||
dispatch!(DeleteApp, handle_delete_app);
|
dispatch!(TAuthMetadataGet, handle_tauth_metadata_get);
|
||||||
|
dispatch!(TAuthMetadataSet, handle_tauth_metadata_set);
|
||||||
|
dispatch!(TAuthMetadataDelete, handle_tauth_metadata_delete);
|
||||||
|
dispatch!(TAuthDisconnectDelete, handle_tauth_disconnect_delete);
|
||||||
|
dispatch!(TAuthGrantList, handle_tauth_grant_list);
|
||||||
|
dispatch!(TAuthGrantRevoke, handle_tauth_grant_revoke);
|
||||||
|
dispatch!(TAuthGrantMetadataSet, handle_tauth_grant_metadata_set);
|
||||||
dispatch!(AccountStateRequest, handle_account_state_request);
|
dispatch!(AccountStateRequest, handle_account_state_request);
|
||||||
dispatch!(AccountStateApplied, handle_account_state_applied);
|
dispatch!(AccountStateApplied, handle_account_state_applied);
|
||||||
dispatch!(ReadNotification, handle_read_notification);
|
dispatch!(ReadNotification, handle_read_notification);
|
||||||
|
|
@ -1871,251 +1891,513 @@ impl OmikronConnection {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_app_identification(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn handle_tauth_authorize(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
let sender_id = match cv.require_sender() {
|
let Some(user_id) = cv
|
||||||
Ok(sender_id) => sender_id,
|
.require_sender()
|
||||||
Err(_) => {
|
.ok()
|
||||||
let _ = self
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
else {
|
||||||
.await;
|
self.send_tauth_error(cv, "missing authenticated user")
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let app_identifier = cv
|
|
||||||
.get_data(DataType::AppIdentifier)
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
let app_public_key = cv
|
|
||||||
.get_data(DataType::AppPublicKey)
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
let Some(user_id) = data_i64(cv, DataType::UserId).filter(|id| *id > 0) else {
|
|
||||||
let _ = self
|
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(request_json) = cv.get_data(DataType::AuthorizationRequest).as_str() else {
|
||||||
let mut trusted = false;
|
self.send_tauth_error(cv, "missing authorization request")
|
||||||
let user = match iota_storage::users::user_manager::get_user(user_id) {
|
.await;
|
||||||
Ok(user) => user,
|
return;
|
||||||
Err(_) => {
|
};
|
||||||
let _ = self
|
let verified =
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
|
match crate::tauth::verify_authorization(&self.http_client, request_json).await {
|
||||||
.await;
|
Ok(verified) => verified,
|
||||||
|
Err(error) => {
|
||||||
|
self.send_tauth_error(cv, &error.to_string()).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let existing = iota_storage::tauth::get_grant(user_id, &verified.request.app_id)
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
let reusable = existing.as_ref().is_some_and(|grant| {
|
||||||
|
grant.domain == verified.request.domain
|
||||||
|
&& grant.app_public_key == verified.manifest.public_key
|
||||||
|
&& iota_storage::tauth::grant_covers(grant, &verified.request.scopes)
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
let approved = cv.get_data(DataType::Enabled) == Some(&DataValue::BoolTrue);
|
||||||
|
let locator = match self.signed_tauth_locator(user_id).await {
|
||||||
|
Ok(locator) => locator,
|
||||||
|
Err(error) => {
|
||||||
|
self.send_tauth_error(cv, &error).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(user) = user {
|
let mut response = CommunicationValue::new(CommunicationType::TAuthAuthorizationResult)
|
||||||
if let Some(pub_k) = user.trusted_apps.get(&app_identifier) {
|
|
||||||
if pub_k == &app_public_key {
|
|
||||||
trusted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if trusted {
|
|
||||||
let challenge = Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
self.app_challenges.insert(
|
|
||||||
sender_id,
|
|
||||||
PendingAppChallenge {
|
|
||||||
challenge: challenge.clone(),
|
|
||||||
user_id,
|
|
||||||
app_identifier: app_identifier.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(app_pub_bundle) =
|
|
||||||
iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key)
|
|
||||||
{
|
|
||||||
let keyring = self.keyring.read().await.as_ref().cloned();
|
|
||||||
if let Some(keyring) = keyring {
|
|
||||||
if let Ok(encrypted_challenge) =
|
|
||||||
crypto_util::encrypt_challenge(&challenge, &app_pub_bundle)
|
|
||||||
{
|
|
||||||
let bundle = keyring.public_key_bundle();
|
|
||||||
let pub_k_b64 = crypto_helper::public_key_bundle_to_base64(&bundle);
|
|
||||||
|
|
||||||
let res = CommunicationValue::new(CommunicationType::AppChallenge)
|
|
||||||
.with_request_id(cv)
|
|
||||||
.with_receiver(sender_id)
|
|
||||||
.add_typed_default(DataType::PublicKey, DataValue::Str(pub_k_b64))
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::Challenge,
|
|
||||||
DataValue::Str(encrypted_challenge),
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = self.send_message(&res).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge)
|
|
||||||
.with_request_id(cv)
|
.with_request_id(cv)
|
||||||
.with_receiver(sender_id);
|
.with_receiver(user_id as u64)
|
||||||
let _ = self.send_message(&res).await;
|
.add_typed_default(
|
||||||
}
|
DataType::AppId,
|
||||||
|
DataValue::Str(verified.request.app_id.clone()),
|
||||||
async fn handle_app_challenge_response(self: Arc<Self>, cv: &CommunicationValue) {
|
)
|
||||||
let sender_id = match cv.require_sender() {
|
.add_typed_default(
|
||||||
Ok(sender_id) => sender_id,
|
DataType::AppName,
|
||||||
Err(_) => {
|
DataValue::Str(verified.manifest.name.clone()),
|
||||||
let _ = self
|
)
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
.add_typed_default(
|
||||||
.await;
|
DataType::Domain,
|
||||||
return;
|
DataValue::Str(verified.request.domain.clone()),
|
||||||
}
|
)
|
||||||
};
|
.add_typed_default(
|
||||||
if let Some((_, pending)) = self.app_challenges.remove(&sender_id) {
|
DataType::RedirectUri,
|
||||||
if let Some(DataValue::Str(response)) = cv.get_data(DataType::Challenge) {
|
DataValue::Str(verified.request.redirect_uri.clone()),
|
||||||
if pending.challenge == *response {
|
)
|
||||||
let authority = match iota_identity::AuthorityId::omega_legacy(
|
.add_typed_default(
|
||||||
&omega_discovery::omega_host(),
|
DataType::State,
|
||||||
) {
|
DataValue::Str(verified.request.state.clone()),
|
||||||
Ok(authority) => authority,
|
)
|
||||||
Err(_) => {
|
.add_typed_default(DataType::Scopes, string_array(&verified.request.scopes))
|
||||||
let _ = self
|
.add_typed_default(DataType::Locator, DataValue::Str(locator.clone()));
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
|
if approved || reusable {
|
||||||
.await;
|
match iota_storage::tauth::issue_code(iota_storage::tauth::AuthorizationGrant {
|
||||||
return;
|
local_user_id: user_id,
|
||||||
}
|
app_id: &verified.request.app_id,
|
||||||
};
|
app_name: &verified.manifest.name,
|
||||||
let user_id = match u64::try_from(pending.user_id) {
|
domain: &verified.request.domain,
|
||||||
Ok(user_id) => user_id,
|
redirect_uri: &verified.request.redirect_uri,
|
||||||
Err(_) => {
|
scopes: &verified.request.scopes,
|
||||||
let _ = self
|
app_public_key: &verified.manifest.public_key,
|
||||||
.send_message(&error_response(
|
manifest_hash: &verified.manifest_hash,
|
||||||
cv,
|
pkce_challenge: &verified.request.pkce_challenge,
|
||||||
CommunicationType::ErrorInvalidData,
|
authorization_request: request_json,
|
||||||
))
|
locator: &locator,
|
||||||
.await;
|
}) {
|
||||||
return;
|
Ok(code) => {
|
||||||
}
|
response = response
|
||||||
};
|
.add_typed_default(DataType::AuthorizationCode, DataValue::Str(code))
|
||||||
let principal = iota_identity::PrincipalId { authority, user_id };
|
}
|
||||||
let principal = match iota_identity::PrincipalStore::get_by_canonical_id(
|
Err(error) => {
|
||||||
&iota_storage::identity::SqlitePrincipalStore,
|
self.send_tauth_error(cv, &error.to_string()).await;
|
||||||
&principal,
|
|
||||||
) {
|
|
||||||
Ok(Some(principal)) => principal,
|
|
||||||
_ => {
|
|
||||||
let _ = self
|
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let connection_id = Uuid::new_v4();
|
|
||||||
iota_auth::HostedSessionRegistrar::new(self.session_manager.clone())
|
|
||||||
.authenticate(
|
|
||||||
connection_id,
|
|
||||||
iota_identity::LocalUserId(pending.user_id),
|
|
||||||
principal.handle,
|
|
||||||
);
|
|
||||||
self.app_sessions.insert(
|
|
||||||
sender_id,
|
|
||||||
AppSession {
|
|
||||||
connection_id,
|
|
||||||
app_identifier: pending.app_identifier,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let res = CommunicationValue::new(CommunicationType::AppIdentificationResponse)
|
|
||||||
.with_request_id(cv)
|
|
||||||
.with_receiver(sender_id);
|
|
||||||
let _ = self.send_message(&res).await;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge)
|
let _ = self.send_message(&response).await;
|
||||||
.with_request_id(cv)
|
|
||||||
.with_receiver(sender_id);
|
|
||||||
let _ = self.send_message(&res).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_save_app_data(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn handle_tauth_exchange_code(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
let sender_id = match cv.require_sender() {
|
let Some(code) = cv.get_data(DataType::AuthorizationCode).as_str() else {
|
||||||
Ok(sender_id) => sender_id,
|
self.send_tauth_error(cv, "missing authorization code")
|
||||||
Err(_) => {
|
.await;
|
||||||
let _ = self
|
return;
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
};
|
||||||
|
let Some(app_id) = cv.get_data(DataType::AppId).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing app ID").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(redirect) = cv.get_data(DataType::RedirectUri).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing redirect URI").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(verifier) = cv.get_data(DataType::CodeVerifier).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing PKCE verifier").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let request_json = match iota_storage::tauth::authorization_request_for_code(code) {
|
||||||
|
Ok(Some(request)) => request,
|
||||||
|
_ => {
|
||||||
|
self.send_tauth_error(cv, "authorization code is invalid, expired, or used")
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let app_data = cv
|
let verified =
|
||||||
.get_data(DataType::AppData)
|
match crate::tauth::verify_authorization(&self.http_client, &request_json).await {
|
||||||
.as_str()
|
Ok(verified)
|
||||||
.unwrap_or("")
|
if verified.request.app_id == app_id
|
||||||
.to_string();
|
&& verified.request.redirect_uri == redirect =>
|
||||||
|
{
|
||||||
if let Some(session) = self.app_sessions.get(&sender_id)
|
verified
|
||||||
&& let Ok(authenticated) = self.session_manager.authorize(
|
}
|
||||||
session.connection_id,
|
Ok(_) => {
|
||||||
&iota_auth::SessionCapability::LocalStorage,
|
self.send_tauth_error(cv, "authorization code binding mismatch")
|
||||||
)
|
.await;
|
||||||
&& let iota_auth::SessionIdentity::Hosted { local_user, .. } = authenticated.identity
|
return;
|
||||||
{
|
}
|
||||||
iota_storage::users::user_manager::save_app_data(
|
Err(error) => {
|
||||||
local_user.0,
|
self.send_tauth_error(cv, &error.to_string()).await;
|
||||||
&session.app_identifier,
|
return;
|
||||||
&app_data,
|
}
|
||||||
);
|
};
|
||||||
}
|
let session = match iota_storage::tauth::exchange_code(code, app_id, redirect, verifier) {
|
||||||
|
Ok(session) => session,
|
||||||
let res = CommunicationValue::new(CommunicationType::SaveAppData)
|
Err(error) => {
|
||||||
.with_request_id(cv)
|
self.send_tauth_error(cv, &error.to_string()).await;
|
||||||
.with_receiver(sender_id);
|
|
||||||
let _ = self.send_message(&res).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_load_app_data(self: Arc<Self>, cv: &CommunicationValue) {
|
|
||||||
let sender_id = match cv.require_sender() {
|
|
||||||
Ok(sender_id) => sender_id,
|
|
||||||
Err(_) => {
|
|
||||||
let _ = self
|
|
||||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
|
||||||
.await;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut app_data = String::new();
|
let principal = iota_storage::tauth::canonical_principal(session.local_user_id)
|
||||||
|
.ok()
|
||||||
if let Some(session) = self.app_sessions.get(&sender_id)
|
.flatten()
|
||||||
&& let Ok(authenticated) = self.session_manager.authorize(
|
.unwrap_or_default();
|
||||||
session.connection_id,
|
let response = CommunicationValue::new(CommunicationType::TAuthExchangeCodeResponse)
|
||||||
&iota_auth::SessionCapability::LocalStorage,
|
|
||||||
)
|
|
||||||
&& let iota_auth::SessionIdentity::Hosted { local_user, .. } = authenticated.identity
|
|
||||||
{
|
|
||||||
app_data = iota_storage::users::user_manager::load_app_data(
|
|
||||||
local_user.0,
|
|
||||||
&session.app_identifier,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = CommunicationValue::new(CommunicationType::LoadAppData)
|
|
||||||
.with_request_id(cv)
|
.with_request_id(cv)
|
||||||
.with_receiver(sender_id)
|
.add_typed_default(DataType::SessionToken, DataValue::Str(session.token))
|
||||||
.add_typed_default(DataType::AppData, DataValue::Str(app_data));
|
.add_typed_default(DataType::AppId, DataValue::Str(verified.request.app_id))
|
||||||
let _ = self.send_message(&res).await;
|
.add_typed_default(DataType::Principal, DataValue::Str(principal))
|
||||||
|
.add_typed_default(DataType::Scopes, string_array(&session.scopes))
|
||||||
|
.add_typed_default(DataType::Locator, DataValue::Str(session.locator));
|
||||||
|
let _ = self.send_message(&response).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_create_app(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn handle_tauth_user(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let Some(session) = self
|
||||||
|
.tauth_session(cv, iota_storage::tauth::Scope::IdentityRead)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let principal = iota_storage::tauth::canonical_principal(session.local_user_id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
match iota_storage::tauth::public_profile(&principal) {
|
||||||
|
Ok(Some(mut profile)) => {
|
||||||
|
match self.signed_tauth_home_node().await {
|
||||||
|
Ok(descriptor) => profile["home_node"] = descriptor,
|
||||||
|
Err(error) => {
|
||||||
|
self.send_tauth_error(cv, &error).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.send_tauth_json(cv, CommunicationType::TAuthUser, profile.to_string())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.send_tauth_error(cv, "user profile is unavailable")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_contacts(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let Some(session) = self
|
||||||
|
.tauth_session(cv, iota_storage::tauth::Scope::ContactsRead)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match iota_storage::tauth::canonical_contact_ids(session.local_user_id) {
|
||||||
|
Ok(contacts) => {
|
||||||
|
self.send_tauth_json(
|
||||||
|
cv,
|
||||||
|
CommunicationType::TAuthContacts,
|
||||||
|
serde_json::to_string(&contacts).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_public_lookup(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
if self
|
||||||
|
.tauth_session(cv, iota_storage::tauth::Scope::IdentityRead)
|
||||||
|
.await
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(principal) = cv.get_data(DataType::Principal).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing principal").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match iota_storage::tauth::public_profile(principal) {
|
||||||
|
Ok(Some(mut profile)) => {
|
||||||
|
if iota_storage::tauth::is_local_principal(principal).unwrap_or(false) {
|
||||||
|
match self.signed_tauth_home_node().await {
|
||||||
|
Ok(descriptor) => profile["home_node"] = descriptor,
|
||||||
|
Err(error) => {
|
||||||
|
self.send_tauth_error(cv, &error).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.send_tauth_json(
|
||||||
|
cv,
|
||||||
|
CommunicationType::TAuthPublicLookup,
|
||||||
|
profile.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Ok(None) => self.send_tauth_error(cv, "principal was not found").await,
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_metadata_get(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let Some(token) = cv.get_data(DataType::SessionToken).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing session token").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match iota_storage::tauth::get_metadata(token) {
|
||||||
|
Ok(value) => {
|
||||||
|
let mut response = CommunicationValue::new(CommunicationType::TAuthMetadataGet)
|
||||||
|
.with_request_id(cv);
|
||||||
|
if let Some(value) = value {
|
||||||
|
response = response.add_typed_default(DataType::Json, DataValue::Str(value));
|
||||||
|
}
|
||||||
|
let _ = self.send_message(&response).await;
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_metadata_set(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let token = cv.get_data(DataType::SessionToken).as_str();
|
||||||
|
let json = cv.get_data(DataType::Json).as_str();
|
||||||
|
match token.zip(json) {
|
||||||
|
Some((token, json)) => match iota_storage::tauth::set_metadata(token, json) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.send_tauth_empty(cv, CommunicationType::TAuthMetadataSet)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
self.send_tauth_error(cv, "missing session token or JSON")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_metadata_delete(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let Some(token) = cv.get_data(DataType::SessionToken).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing session token").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match iota_storage::tauth::delete_metadata(token) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.send_tauth_empty(cv, CommunicationType::TAuthMetadataDelete)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_disconnect_delete(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let Some(token) = cv.get_data(DataType::SessionToken).as_str() else {
|
||||||
|
self.send_tauth_error(cv, "missing session token").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match iota_storage::tauth::disconnect_and_delete(token) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.send_tauth_empty(cv, CommunicationType::TAuthDisconnectDelete)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_grant_list(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let Some(user_id) = cv
|
||||||
|
.require_sender()
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
|
else {
|
||||||
|
self.send_tauth_error(cv, "missing authenticated user")
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match iota_storage::tauth::list_grants(user_id) {
|
||||||
|
Ok(grants) => {
|
||||||
|
self.send_tauth_json(
|
||||||
|
cv,
|
||||||
|
CommunicationType::TAuthGrantResponse,
|
||||||
|
serde_json::to_string(&grants).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_grant_revoke(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let user_id = cv
|
||||||
|
.require_sender()
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| i64::try_from(value).ok());
|
||||||
|
let app_id = cv.get_data(DataType::AppId).as_str();
|
||||||
|
match user_id.zip(app_id) {
|
||||||
|
Some((user_id, app_id)) => match iota_storage::tauth::revoke_grant(user_id, app_id) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.send_tauth_empty(cv, CommunicationType::TAuthGrantResponse)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
},
|
||||||
|
None => self.send_tauth_error(cv, "missing user or app ID").await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tauth_grant_metadata_set(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
let user_id = cv
|
||||||
|
.require_sender()
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| i64::try_from(value).ok());
|
||||||
|
let app_id = cv.get_data(DataType::AppId).as_str();
|
||||||
|
let json = cv.get_data(DataType::Json).as_str();
|
||||||
|
match user_id.zip(app_id).zip(json) {
|
||||||
|
Some(((user_id, app_id), json)) => {
|
||||||
|
match iota_storage::tauth::set_metadata_for_user(user_id, app_id, json) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.send_tauth_empty(cv, CommunicationType::TAuthGrantResponse)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(error) => self.send_tauth_error(cv, &error.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.send_tauth_error(cv, "missing user, app ID, or JSON")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tauth_session(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
cv: &CommunicationValue,
|
||||||
|
scope: iota_storage::tauth::Scope,
|
||||||
|
) -> Option<iota_storage::tauth::Session> {
|
||||||
|
let token = cv.get_data(DataType::SessionToken).as_str();
|
||||||
|
match token.map(|token| iota_storage::tauth::authorize_session(token, scope)) {
|
||||||
|
Some(Ok(session)) => Some(session),
|
||||||
|
Some(Err(error)) => {
|
||||||
|
self.clone().send_tauth_error(cv, &error.to_string()).await;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.clone()
|
||||||
|
.send_tauth_error(cv, "missing session token")
|
||||||
|
.await;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn signed_tauth_locator(&self, user_id: i64) -> Result<String, String> {
|
||||||
|
let config = CONFIG.load();
|
||||||
|
let target_iota_id = config
|
||||||
|
.iota_id
|
||||||
|
.ok_or_else(|| "Iota ID is unavailable".to_string())?;
|
||||||
|
let omikron_id = config
|
||||||
|
.omikron_id
|
||||||
|
.ok_or_else(|| "Omikron ID is unavailable".to_string())?;
|
||||||
|
let host = config
|
||||||
|
.omikron_host
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| "Omikron host is unavailable".to_string())?;
|
||||||
|
let port = config
|
||||||
|
.omikron_port
|
||||||
|
.ok_or_else(|| "Omikron port is unavailable".to_string())?;
|
||||||
|
let omikron_key = mtp::files::load_public_key_bundle(&omikron_public_key_path(omikron_id))
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let omikron_public_key = omikron_key
|
||||||
|
.try_to_base64()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let keyring = self
|
||||||
|
.keyring
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| "Iota keyring is unavailable".to_string())?;
|
||||||
|
let iota_public_key = keyring
|
||||||
|
.public_key_bundle()
|
||||||
|
.try_to_base64()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let principal = iota_storage::tauth::canonical_principal(user_id)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.ok_or_else(|| "canonical principal is unavailable".to_string())?;
|
||||||
|
let omikron_url = format!("https://{host}:{port}");
|
||||||
|
let issued_at = iota_storage::tauth::now_seconds();
|
||||||
|
let unsigned = TAuthLocatorUnsigned {
|
||||||
|
version: 1,
|
||||||
|
principal: &principal,
|
||||||
|
omikron_url: &omikron_url,
|
||||||
|
omikron_public_key: &omikron_public_key,
|
||||||
|
target_iota_id,
|
||||||
|
iota_public_key: &iota_public_key,
|
||||||
|
issued_at,
|
||||||
|
};
|
||||||
|
let signer =
|
||||||
|
Ed25519Signer::new(&keyring.sig_cl_secret_key).map_err(|error| error.to_string())?;
|
||||||
|
let signature = STANDARD.encode(
|
||||||
|
signer
|
||||||
|
.sign(&serde_json::to_vec(&unsigned).unwrap())
|
||||||
|
.map_err(|error| error.to_string())?,
|
||||||
|
);
|
||||||
|
serde_json::to_string(&TAuthLocator {
|
||||||
|
version: 1,
|
||||||
|
principal: &principal,
|
||||||
|
omikron_url: &omikron_url,
|
||||||
|
omikron_public_key: &omikron_public_key,
|
||||||
|
target_iota_id,
|
||||||
|
iota_public_key: &iota_public_key,
|
||||||
|
issued_at,
|
||||||
|
signature,
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn signed_tauth_home_node(&self) -> Result<serde_json::Value, String> {
|
||||||
|
let config = CONFIG.load();
|
||||||
|
let host = config
|
||||||
|
.omikron_host
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| "Omikron host is unavailable".to_string())?;
|
||||||
|
let port = config
|
||||||
|
.omikron_port
|
||||||
|
.ok_or_else(|| "Omikron port is unavailable".to_string())?;
|
||||||
|
let relay =
|
||||||
|
AuthorityLocator::new(format!("{host}:{port}")).map_err(|error| error.to_string())?;
|
||||||
|
let identity = self
|
||||||
|
.load_or_migrate_keyring()
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis()
|
||||||
|
.min(i64::MAX as u128) as i64;
|
||||||
|
let descriptor = iota_storage::node_directory::SqliteNodeDirectory
|
||||||
|
.ensure_local_descriptor(&identity, Vec::new(), vec![relay], now)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.to_wire_v1()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
serde_json::to_value(descriptor).map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_tauth_empty(self: Arc<Self>, cv: &CommunicationValue, kind: CommunicationType) {
|
||||||
let _ = self
|
let _ = self
|
||||||
.send_message(&message_handlers::handle_create_app(cv))
|
.send_message(&CommunicationValue::new(kind).with_request_id(cv))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_delete_app(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn send_tauth_json(
|
||||||
let _ = self
|
self: Arc<Self>,
|
||||||
.send_message(&message_handlers::handle_delete_app(cv))
|
cv: &CommunicationValue,
|
||||||
.await;
|
kind: CommunicationType,
|
||||||
|
json: String,
|
||||||
|
) {
|
||||||
|
let response = CommunicationValue::new(kind)
|
||||||
|
.with_request_id(cv)
|
||||||
|
.add_typed_default(DataType::Json, DataValue::Str(json));
|
||||||
|
let _ = self.send_message(&response).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_tauth_error(self: Arc<Self>, cv: &CommunicationValue, message: &str) {
|
||||||
|
let response = error_response(cv, CommunicationType::ErrorInvalidData)
|
||||||
|
.add_typed_default(DataType::Message, DataValue::Str(message.to_owned()));
|
||||||
|
let _ = self.send_message(&response).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_account_state_request(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn handle_account_state_request(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
|
|
@ -3101,8 +3383,7 @@ impl OmikronClient for OmikronConnection {
|
||||||
reconnect_on_close: self.reconnect_on_close.clone(),
|
reconnect_on_close: self.reconnect_on_close.clone(),
|
||||||
auth_failure: self.auth_failure.clone(),
|
auth_failure: self.auth_failure.clone(),
|
||||||
keyring: self.keyring.clone(),
|
keyring: self.keyring.clone(),
|
||||||
app_challenges: self.app_challenges.clone(),
|
http_client: self.http_client.clone(),
|
||||||
app_sessions: self.app_sessions.clone(),
|
|
||||||
session_manager: self.session_manager.clone(),
|
session_manager: self.session_manager.clone(),
|
||||||
handler_semaphore: self.handler_semaphore.clone(),
|
handler_semaphore: self.handler_semaphore.clone(),
|
||||||
cancellation: self.cancellation.clone(),
|
cancellation: self.cancellation.clone(),
|
||||||
|
|
@ -3128,8 +3409,7 @@ impl OmikronClient for OmikronConnection {
|
||||||
reconnect_on_close: self.reconnect_on_close.clone(),
|
reconnect_on_close: self.reconnect_on_close.clone(),
|
||||||
auth_failure: self.auth_failure.clone(),
|
auth_failure: self.auth_failure.clone(),
|
||||||
keyring: self.keyring.clone(),
|
keyring: self.keyring.clone(),
|
||||||
app_challenges: self.app_challenges.clone(),
|
http_client: self.http_client.clone(),
|
||||||
app_sessions: self.app_sessions.clone(),
|
|
||||||
session_manager: self.session_manager.clone(),
|
session_manager: self.session_manager.clone(),
|
||||||
handler_semaphore: self.handler_semaphore.clone(),
|
handler_semaphore: self.handler_semaphore.clone(),
|
||||||
cancellation: self.cancellation.clone(),
|
cancellation: self.cancellation.clone(),
|
||||||
|
|
|
||||||
490
omikron-connector/src/tauth.rs
Normal file
490
omikron-connector/src/tauth.rs
Normal file
|
|
@ -0,0 +1,490 @@
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
|
use iota_storage::tauth::{self, Scope};
|
||||||
|
use mtp::crypto::{PublicKeyBundle, verify_ed25519};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use trust_dns_resolver::{
|
||||||
|
TokioAsyncResolver,
|
||||||
|
config::{ResolverConfig, ResolverOpts},
|
||||||
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SignedAuthorizationRequest {
|
||||||
|
pub version: u16,
|
||||||
|
pub app_id: String,
|
||||||
|
pub domain: String,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
pub scopes: Vec<String>,
|
||||||
|
pub state: String,
|
||||||
|
pub pkce_challenge: String,
|
||||||
|
pub expires_at: i64,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AppManifest {
|
||||||
|
pub version: u16,
|
||||||
|
pub app_id: String,
|
||||||
|
pub public_key: String,
|
||||||
|
pub name: String,
|
||||||
|
pub domain: String,
|
||||||
|
pub redirects: Vec<String>,
|
||||||
|
pub owner_certificate: String,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OwnerCertificate {
|
||||||
|
pub version: u16,
|
||||||
|
pub app_id: String,
|
||||||
|
pub app_public_key: String,
|
||||||
|
pub owner_principal: String,
|
||||||
|
pub owner_public_key: String,
|
||||||
|
pub owner_iota_id: u64,
|
||||||
|
pub issued_at: i64,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct VerifiedAuthorization {
|
||||||
|
pub request: SignedAuthorizationRequest,
|
||||||
|
pub manifest: AppManifest,
|
||||||
|
pub manifest_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum VerificationError {
|
||||||
|
#[error("invalid signed authorization request: {0}")]
|
||||||
|
Request(String),
|
||||||
|
#[error("TAuth DNS discovery failed: {0}")]
|
||||||
|
Dns(String),
|
||||||
|
#[error("TAuth manifest fetch failed: {0}")]
|
||||||
|
Http(String),
|
||||||
|
#[error("TAuth manifest verification failed: {0}")]
|
||||||
|
Manifest(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UnsignedAuthorizationRequest<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
domain: &'a str,
|
||||||
|
redirect_uri: &'a str,
|
||||||
|
scopes: &'a [String],
|
||||||
|
state: &'a str,
|
||||||
|
pkce_challenge: &'a str,
|
||||||
|
expires_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UnsignedManifest<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
public_key: &'a str,
|
||||||
|
name: &'a str,
|
||||||
|
domain: &'a str,
|
||||||
|
redirects: &'a [String],
|
||||||
|
owner_certificate: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UnsignedOwnerCertificate<'a> {
|
||||||
|
version: u16,
|
||||||
|
app_id: &'a str,
|
||||||
|
app_public_key: &'a str,
|
||||||
|
owner_principal: &'a str,
|
||||||
|
owner_public_key: &'a str,
|
||||||
|
owner_iota_id: u64,
|
||||||
|
issued_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn verify_authorization(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
request_json: &str,
|
||||||
|
) -> Result<VerifiedAuthorization, VerificationError> {
|
||||||
|
let request: SignedAuthorizationRequest = serde_json::from_str(request_json)
|
||||||
|
.map_err(|error| VerificationError::Request(error.to_string()))?;
|
||||||
|
validate_request_shape(&request)?;
|
||||||
|
let result = verify_discovery(client, &request).await;
|
||||||
|
if matches!(
|
||||||
|
&result,
|
||||||
|
Err(VerificationError::Dns(_)
|
||||||
|
| VerificationError::Http(_)
|
||||||
|
| VerificationError::Manifest(_))
|
||||||
|
) {
|
||||||
|
let _ = tauth::mark_insecure(&request.app_id, &request.domain);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_discovery(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
request: &SignedAuthorizationRequest,
|
||||||
|
) -> Result<VerifiedAuthorization, VerificationError> {
|
||||||
|
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
|
||||||
|
let lookup = resolver
|
||||||
|
.txt_lookup(format!("_tauth.{}", request.domain))
|
||||||
|
.await
|
||||||
|
.map_err(|error| VerificationError::Dns(error.to_string()))?;
|
||||||
|
let txt = lookup
|
||||||
|
.iter()
|
||||||
|
.map(|record| {
|
||||||
|
record
|
||||||
|
.txt_data()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|part| part.iter().copied())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.find_map(|bytes| {
|
||||||
|
String::from_utf8(bytes)
|
||||||
|
.ok()
|
||||||
|
.filter(|value| value.starts_with("v=TAUTH1;"))
|
||||||
|
})
|
||||||
|
.ok_or_else(|| VerificationError::Dns("missing v=TAUTH1 TXT value".into()))?;
|
||||||
|
let txt_app_id = txt_value(&txt, "app")
|
||||||
|
.ok_or_else(|| VerificationError::Dns("TXT app fingerprint is missing".into()))?;
|
||||||
|
let txt_manifest_hash = txt_value(&txt, "manifest")
|
||||||
|
.ok_or_else(|| VerificationError::Dns("TXT manifest hash is missing".into()))?;
|
||||||
|
if txt_app_id != request.app_id || !is_sha256_hex(txt_manifest_hash) {
|
||||||
|
return Err(VerificationError::Dns(
|
||||||
|
"TXT fingerprint or manifest hash is invalid".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_url = Url::parse(&format!(
|
||||||
|
"https://{}/.well-known/tauth.json",
|
||||||
|
request.domain
|
||||||
|
))
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
let response = client
|
||||||
|
.get(manifest_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|error| VerificationError::Http(error.to_string()))?
|
||||||
|
.error_for_status()
|
||||||
|
.map_err(|error| VerificationError::Http(error.to_string()))?;
|
||||||
|
let bytes = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|error| VerificationError::Http(error.to_string()))?;
|
||||||
|
let manifest_hash = hex::encode(Sha256::digest(&bytes));
|
||||||
|
if manifest_hash != txt_manifest_hash.to_ascii_lowercase() {
|
||||||
|
return Err(VerificationError::Manifest(
|
||||||
|
"manifest hash does not match DNS".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let manifest: AppManifest = serde_json::from_slice(&bytes)
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
validate_manifest(request, &manifest)?;
|
||||||
|
Ok(VerifiedAuthorization {
|
||||||
|
request: request.clone(),
|
||||||
|
manifest,
|
||||||
|
manifest_hash,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_request_shape(request: &SignedAuthorizationRequest) -> Result<(), VerificationError> {
|
||||||
|
if request.version != 1 || !is_sha256_hex(&request.app_id) || request.state.is_empty() {
|
||||||
|
return Err(VerificationError::Request(
|
||||||
|
"version, app ID, or state is invalid".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let domain = request.domain.trim().to_ascii_lowercase();
|
||||||
|
if domain != request.domain || domain.is_empty() || domain.contains('/') {
|
||||||
|
return Err(VerificationError::Request("domain is not canonical".into()));
|
||||||
|
}
|
||||||
|
validate_redirect(&request.redirect_uri)?;
|
||||||
|
tauth::normalize_scopes(&request.scopes)
|
||||||
|
.map_err(|error| VerificationError::Request(error.to_string()))?;
|
||||||
|
if request.pkce_challenge.len() < 43 || request.pkce_challenge.len() > 128 {
|
||||||
|
return Err(VerificationError::Request(
|
||||||
|
"PKCE challenge length is invalid".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let now = tauth::now_seconds();
|
||||||
|
if request.expires_at <= now || request.expires_at > now + 300 {
|
||||||
|
return Err(VerificationError::Request(
|
||||||
|
"request is expired or too long-lived".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_manifest(
|
||||||
|
request: &SignedAuthorizationRequest,
|
||||||
|
manifest: &AppManifest,
|
||||||
|
) -> Result<(), VerificationError> {
|
||||||
|
if manifest.version != 1
|
||||||
|
|| manifest.app_id != request.app_id
|
||||||
|
|| manifest.domain != request.domain
|
||||||
|
|| manifest.name.trim().is_empty()
|
||||||
|
|| !manifest
|
||||||
|
.redirects
|
||||||
|
.iter()
|
||||||
|
.any(|redirect| redirect == &request.redirect_uri)
|
||||||
|
{
|
||||||
|
return Err(VerificationError::Manifest(
|
||||||
|
"manifest fields do not match request".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for redirect in &manifest.redirects {
|
||||||
|
validate_redirect(redirect)?;
|
||||||
|
}
|
||||||
|
let public_key = PublicKeyBundle::from_base64(&manifest.public_key)
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
if iota_util::ta::app_id(&public_key) != request.app_id {
|
||||||
|
return Err(VerificationError::Manifest(
|
||||||
|
"public key fingerprint does not match app ID".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let owner_bytes = STANDARD
|
||||||
|
.decode(&manifest.owner_certificate)
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
let owner: OwnerCertificate = serde_json::from_slice(&owner_bytes)
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
validate_owner_certificate(&owner, manifest)?;
|
||||||
|
verify(
|
||||||
|
&public_key,
|
||||||
|
&serde_json::to_vec(&UnsignedManifest {
|
||||||
|
version: manifest.version,
|
||||||
|
app_id: &manifest.app_id,
|
||||||
|
public_key: &manifest.public_key,
|
||||||
|
name: &manifest.name,
|
||||||
|
domain: &manifest.domain,
|
||||||
|
redirects: &manifest.redirects,
|
||||||
|
owner_certificate: &manifest.owner_certificate,
|
||||||
|
})
|
||||||
|
.expect("manifest fields serialize"),
|
||||||
|
&manifest.signature,
|
||||||
|
"manifest",
|
||||||
|
)?;
|
||||||
|
let request_signature = STANDARD
|
||||||
|
.decode(&request.signature)
|
||||||
|
.map_err(|error| VerificationError::Request(error.to_string()))?;
|
||||||
|
verify_ed25519(
|
||||||
|
&public_key.sig_cl_public_key,
|
||||||
|
&serde_json::to_vec(&UnsignedAuthorizationRequest {
|
||||||
|
version: request.version,
|
||||||
|
app_id: &request.app_id,
|
||||||
|
domain: &request.domain,
|
||||||
|
redirect_uri: &request.redirect_uri,
|
||||||
|
scopes: &request.scopes,
|
||||||
|
state: &request.state,
|
||||||
|
pkce_challenge: &request.pkce_challenge,
|
||||||
|
expires_at: request.expires_at,
|
||||||
|
})
|
||||||
|
.expect("authorization fields serialize"),
|
||||||
|
&request_signature,
|
||||||
|
)
|
||||||
|
.map_err(|_| VerificationError::Request("authorization request signature is invalid".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_owner_certificate(
|
||||||
|
owner: &OwnerCertificate,
|
||||||
|
manifest: &AppManifest,
|
||||||
|
) -> Result<(), VerificationError> {
|
||||||
|
if owner.version != 1
|
||||||
|
|| owner.owner_iota_id == 0
|
||||||
|
|| owner.owner_principal.is_empty()
|
||||||
|
|| owner.app_id != manifest.app_id
|
||||||
|
|| owner.app_public_key != manifest.public_key
|
||||||
|
{
|
||||||
|
return Err(VerificationError::Manifest(
|
||||||
|
"owner certificate does not bind this app".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let owner_key = PublicKeyBundle::from_base64(&owner.owner_public_key)
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
verify(
|
||||||
|
&owner_key,
|
||||||
|
&serde_json::to_vec(&UnsignedOwnerCertificate {
|
||||||
|
version: owner.version,
|
||||||
|
app_id: &owner.app_id,
|
||||||
|
app_public_key: &owner.app_public_key,
|
||||||
|
owner_principal: &owner.owner_principal,
|
||||||
|
owner_public_key: &owner.owner_public_key,
|
||||||
|
owner_iota_id: owner.owner_iota_id,
|
||||||
|
issued_at: owner.issued_at,
|
||||||
|
})
|
||||||
|
.expect("certificate fields serialize"),
|
||||||
|
&owner.signature,
|
||||||
|
"owner certificate",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(
|
||||||
|
key: &PublicKeyBundle,
|
||||||
|
message: &[u8],
|
||||||
|
encoded_signature: &str,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<(), VerificationError> {
|
||||||
|
let signature = STANDARD
|
||||||
|
.decode(encoded_signature)
|
||||||
|
.map_err(|error| VerificationError::Manifest(error.to_string()))?;
|
||||||
|
verify_ed25519(&key.sig_cl_public_key, message, &signature)
|
||||||
|
.map_err(|_| VerificationError::Manifest(format!("{label} signature is invalid")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_redirect(value: &str) -> Result<(), VerificationError> {
|
||||||
|
let url = Url::parse(value).map_err(|error| VerificationError::Request(error.to_string()))?;
|
||||||
|
let localhost = matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "[::1]"));
|
||||||
|
if url.host_str().is_none()
|
||||||
|
|| !url.username().is_empty()
|
||||||
|
|| url.password().is_some()
|
||||||
|
|| (url.scheme() != "https" && !(url.scheme() == "http" && localhost))
|
||||||
|
|| url.fragment().is_some()
|
||||||
|
|| url
|
||||||
|
.query_pairs()
|
||||||
|
.any(|(key, _)| matches!(key.as_ref(), "code" | "state" | "locator" | "error"))
|
||||||
|
{
|
||||||
|
return Err(VerificationError::Request(
|
||||||
|
"redirect URL must be exact HTTPS without a fragment".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn txt_value<'a>(record: &'a str, key: &str) -> Option<&'a str> {
|
||||||
|
record.split(';').find_map(|field| {
|
||||||
|
field
|
||||||
|
.split_once('=')
|
||||||
|
.filter(|(name, value)| *name == key && !value.is_empty())
|
||||||
|
.map(|(_, value)| value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sha256_hex(value: &str) -> bool {
|
||||||
|
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scope_for_command(command: mtp::codec::CommunicationType) -> Option<Scope> {
|
||||||
|
use mtp::codec::CommunicationType;
|
||||||
|
match command {
|
||||||
|
CommunicationType::TAuthUser | CommunicationType::TAuthPublicLookup => {
|
||||||
|
Some(Scope::IdentityRead)
|
||||||
|
}
|
||||||
|
CommunicationType::TAuthContacts => Some(Scope::ContactsRead),
|
||||||
|
CommunicationType::TAuthMetadataGet => Some(Scope::MetadataRead),
|
||||||
|
CommunicationType::TAuthMetadataSet | CommunicationType::TAuthMetadataDelete => {
|
||||||
|
Some(Scope::MetadataWrite)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use mtp::crypto::{Ed25519Signer, Keyring, SignatureScheme};
|
||||||
|
|
||||||
|
fn signed_authorization() -> (SignedAuthorizationRequest, AppManifest) {
|
||||||
|
let app_keyring = Keyring::generate();
|
||||||
|
let owner_keyring = Keyring::generate();
|
||||||
|
let app_public_key = app_keyring.public_key_bundle().try_to_base64().unwrap();
|
||||||
|
let owner_public_key = owner_keyring.public_key_bundle().try_to_base64().unwrap();
|
||||||
|
let app_id = iota_util::ta::app_id(&app_keyring.public_key_bundle());
|
||||||
|
let owner_unsigned = UnsignedOwnerCertificate {
|
||||||
|
version: 1,
|
||||||
|
app_id: &app_id,
|
||||||
|
app_public_key: &app_public_key,
|
||||||
|
owner_principal: "iota:owner#7",
|
||||||
|
owner_public_key: &owner_public_key,
|
||||||
|
owner_iota_id: 7,
|
||||||
|
issued_at: tauth::now_seconds(),
|
||||||
|
};
|
||||||
|
let owner_signer = Ed25519Signer::new(&owner_keyring.sig_cl_secret_key).unwrap();
|
||||||
|
let owner = OwnerCertificate {
|
||||||
|
version: owner_unsigned.version,
|
||||||
|
app_id: owner_unsigned.app_id.to_owned(),
|
||||||
|
app_public_key: owner_unsigned.app_public_key.to_owned(),
|
||||||
|
owner_principal: owner_unsigned.owner_principal.to_owned(),
|
||||||
|
owner_public_key: owner_unsigned.owner_public_key.to_owned(),
|
||||||
|
owner_iota_id: owner_unsigned.owner_iota_id,
|
||||||
|
issued_at: owner_unsigned.issued_at,
|
||||||
|
signature: STANDARD.encode(
|
||||||
|
owner_signer
|
||||||
|
.sign(&serde_json::to_vec(&owner_unsigned).unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let owner_certificate = STANDARD.encode(serde_json::to_vec(&owner).unwrap());
|
||||||
|
let redirects = vec!["https://app.example/callback".to_owned()];
|
||||||
|
let manifest_unsigned = UnsignedManifest {
|
||||||
|
version: 1,
|
||||||
|
app_id: &app_id,
|
||||||
|
public_key: &app_public_key,
|
||||||
|
name: "Example",
|
||||||
|
domain: "app.example",
|
||||||
|
redirects: &redirects,
|
||||||
|
owner_certificate: &owner_certificate,
|
||||||
|
};
|
||||||
|
let app_signer = Ed25519Signer::new(&app_keyring.sig_cl_secret_key).unwrap();
|
||||||
|
let manifest = AppManifest {
|
||||||
|
version: manifest_unsigned.version,
|
||||||
|
app_id: manifest_unsigned.app_id.to_owned(),
|
||||||
|
public_key: manifest_unsigned.public_key.to_owned(),
|
||||||
|
name: manifest_unsigned.name.to_owned(),
|
||||||
|
domain: manifest_unsigned.domain.to_owned(),
|
||||||
|
redirects: manifest_unsigned.redirects.to_vec(),
|
||||||
|
owner_certificate: manifest_unsigned.owner_certificate.to_owned(),
|
||||||
|
signature: STANDARD.encode(
|
||||||
|
app_signer
|
||||||
|
.sign(&serde_json::to_vec(&manifest_unsigned).unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let scopes = vec!["identity.read".to_owned()];
|
||||||
|
let request_unsigned = UnsignedAuthorizationRequest {
|
||||||
|
version: 1,
|
||||||
|
app_id: &app_id,
|
||||||
|
domain: "app.example",
|
||||||
|
redirect_uri: &redirects[0],
|
||||||
|
scopes: &scopes,
|
||||||
|
state: "state",
|
||||||
|
pkce_challenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
expires_at: tauth::now_seconds() + 60,
|
||||||
|
};
|
||||||
|
let request = SignedAuthorizationRequest {
|
||||||
|
version: request_unsigned.version,
|
||||||
|
app_id: request_unsigned.app_id.to_owned(),
|
||||||
|
domain: request_unsigned.domain.to_owned(),
|
||||||
|
redirect_uri: request_unsigned.redirect_uri.to_owned(),
|
||||||
|
scopes: request_unsigned.scopes.to_vec(),
|
||||||
|
state: request_unsigned.state.to_owned(),
|
||||||
|
pkce_challenge: request_unsigned.pkce_challenge.to_owned(),
|
||||||
|
expires_at: request_unsigned.expires_at,
|
||||||
|
signature: STANDARD.encode(
|
||||||
|
app_signer
|
||||||
|
.sign(&serde_json::to_vec(&request_unsigned).unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
(request, manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signatures_and_exact_redirect_are_bound() {
|
||||||
|
let (request, manifest) = signed_authorization();
|
||||||
|
validate_request_shape(&request).unwrap();
|
||||||
|
validate_manifest(&request, &manifest).unwrap();
|
||||||
|
|
||||||
|
let mut tampered = request.clone();
|
||||||
|
tampered.state = "different".into();
|
||||||
|
assert!(validate_manifest(&tampered, &manifest).is_err());
|
||||||
|
|
||||||
|
let mut wrong_redirect = request;
|
||||||
|
wrong_redirect.redirect_uri = "https://app.example/other".into();
|
||||||
|
assert!(validate_manifest(&wrong_redirect, &manifest).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expired_requests_and_unsafe_redirects_fail_before_discovery() {
|
||||||
|
let (mut request, _) = signed_authorization();
|
||||||
|
request.expires_at = tauth::now_seconds();
|
||||||
|
assert!(validate_request_shape(&request).is_err());
|
||||||
|
request.expires_at = tauth::now_seconds() + 60;
|
||||||
|
request.redirect_uri = "https://app.example/callback#fragment".into();
|
||||||
|
assert!(validate_request_shape(&request).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
5
todo.md
Normal file
5
todo.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Direct Iota TAuth
|
||||||
|
|
||||||
|
Add `DirectIota` connection mode to binary `.ta` format and Rust SDK.
|
||||||
|
|
||||||
|
Direct sessions must implement same TAuth MTP messages, signed authorization checks, exact redirect and PKCE binding, discovery revalidation, scope enforcement, one-time codes, metadata limits, and atomic revocation used by Omikron-routed sessions. Direct raw MTP access must not bypass grant checks.
|
||||||
Loading…
Reference in a new issue