[WIP] 0.3.0 mtp update

This commit is contained in:
Alex Emmet 2026-08-18 22:37:14 +02:00
commit 7709fe599a
No known key found for this signature in database
19 changed files with 1009 additions and 278 deletions

View file

@ -1,10 +1,15 @@
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use super::super::connection::{
MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat,
};
use crate::{
db::{iota_repo, user_repo},
models::{IotaId, UserId},
state::AccountChallengeOperation,
};
use mtp::{codec::{CommunicationType, CommunicationValue, DataType, DataValue}, crypto::{verify_ed25519, verify_ml_dsa}};
use mtp::{
codec::{CommunicationType, CommunicationValue, DataType, DataValue},
crypto::{verify_ed25519, verify_ml_dsa},
};
use std::sync::Arc;
async fn delete(
@ -67,14 +72,22 @@ pub async fn release_from_iota(
let previous_iota = user.iota_id;
match user_repo::change_iota_id(user.id, None).await {
Ok(()) => {
if let Some(iota) = previous_iota { crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; }
connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())).await
},
Err(error) => connection
.send(&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.get_id())
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())))
.await,
if let Some(iota) = previous_iota {
crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
}
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id()))
.await
}
Err(error) => {
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.get_id())
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
)
.await
}
}
}
@ -87,43 +100,121 @@ fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> V
payload
}
pub async fn attach_begin(connection: Arc<OmikronConnection>, value: CommunicationValue) -> OmikronResult<()> {
let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else {
return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await;
pub async fn attach_begin(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId)
.await;
};
if user_repo::get_by_user_id(UserId::from(user_id)).await.is_err() {
return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await;
if user_repo::get_by_user_id(UserId::from(user_id))
.await
.is_err()
{
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
}
let requester = value.get_sender() as i64;
let nonce = connection.state().issue_challenge(AccountChallengeOperation::Attach, user_id, requester);
connection.send(&CommunicationValue::new(CommunicationType::AttachUserChallenge).with_id(value.get_id())
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into()))).await
let nonce =
connection
.state()
.issue_challenge(AccountChallengeOperation::Attach, user_id, requester);
connection
.send(
&CommunicationValue::new(CommunicationType::AttachUserChallenge)
.with_id(value.get_id())
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())),
)
.await
}
pub async fn attach_complete(connection: Arc<OmikronConnection>, value: CommunicationValue) -> OmikronResult<()> {
let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; };
pub async fn attach_complete(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId)
.await;
};
let requester = value.get_sender() as i64;
let Some(nonce) = value.get_data(DataType::ServerNonce).as_signed_number().and_then(|v| u64::try_from(v).ok()) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; };
let Some(nonce) = value
.get_data(DataType::ServerNonce)
.as_signed_number()
.and_then(|v| u64::try_from(v).ok())
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
};
let signature = value.get_data(DataType::Signature).as_bytes();
let pq_signature = value.get_data(DataType::PqSignature).as_bytes();
let (Some(signature), Some(pq_signature)) = (signature, pq_signature) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; };
if !connection.state().consume_challenge(AccountChallengeOperation::Attach, user_id, requester, nonce) { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; }
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; };
let (Some(signature), Some(pq_signature)) = (signature, pq_signature) else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
};
if !connection.state().consume_challenge(
AccountChallengeOperation::Attach,
user_id,
requester,
nonce,
) {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
}
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
};
let payload = lifecycle_payload(b"tensamin:user-attach:v1\0", user_id, requester, nonce);
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err() || verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err() { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated).await; }
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err()
|| verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err()
{
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated)
.await;
}
let previous_iota = user.iota_id;
match user_repo::change_iota_id(user.id, Some(IotaId::from(requester))).await {
Ok(()) => {
if let Some(iota) = previous_iota.filter(|id| id.0 != requester) { crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; }
if let Some(iota) = previous_iota.filter(|id| id.0 != requester) {
crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
}
crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await;
connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())).await
},
Err(_) => connection.send_error_response(value.get_id(), CommunicationType::ErrorInternal).await,
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id()))
.await
}
Err(_) => {
connection
.send_error_response(value.get_id(), CommunicationType::ErrorInternal)
.await
}
}
}
async fn complete_delete(connection: Arc<OmikronConnection>, value: CommunicationValue, user_id: UserId) -> OmikronResult<()> {
async fn complete_delete(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
user_id: UserId,
) -> OmikronResult<()> {
match user_repo::delete_user_with_pending_erasure(user_id).await {
Ok(iota_id) => {
let cleanup_pending = iota_id.is_some();
@ -131,45 +222,160 @@ async fn complete_delete(connection: Arc<OmikronConnection>, value: Communicatio
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id.0).await;
crate::transport::omikron_manager::deliver_pending_erasures(iota_id.0).await;
}
connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())
.add_typed_default(DataType::CleanupPending, DataValue::Bool(cleanup_pending))).await
connection
.send(
&CommunicationValue::new(CommunicationType::Success)
.with_id(value.get_id())
.add_typed_default(
DataType::CleanupPending,
DataValue::Bool(cleanup_pending),
),
)
.await
}
Err(crate::error::OmegaError::NotFound) => {
connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await
}
Err(error) => {
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.get_id())
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
)
.await
}
Err(crate::error::OmegaError::NotFound) => connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await,
Err(error) => connection.send(&CommunicationValue::new(CommunicationType::ErrorInternal).with_id(value.get_id()).add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string()))).await,
}
}
pub async fn delete_credential_begin(connection: Arc<OmikronConnection>, value: CommunicationValue) -> OmikronResult<()> {
let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else {
return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await;
pub async fn delete_credential_begin(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId)
.await;
};
if user_repo::get_by_user_id(UserId::from(user_id)).await.is_err() { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; }
if user_repo::get_by_user_id(UserId::from(user_id))
.await
.is_err()
{
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
}
let requester = value.get_sender() as i64;
let nonce = connection.state().issue_challenge(AccountChallengeOperation::Delete, user_id, requester);
connection.send(&CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge).with_id(value.get_id())
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into()))).await
let nonce =
connection
.state()
.issue_challenge(AccountChallengeOperation::Delete, user_id, requester);
connection
.send(
&CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge)
.with_id(value.get_id())
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())),
)
.await
}
pub async fn delete_credential_complete(connection: Arc<OmikronConnection>, value: CommunicationValue) -> OmikronResult<()> {
let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; };
pub async fn delete_credential_complete(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId)
.await;
};
let requester = value.get_sender() as i64;
let Some(nonce) = value.get_data(DataType::ServerNonce).as_signed_number().and_then(|v| u64::try_from(v).ok()) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; };
let (Some(signature), Some(pq_signature)) = (value.get_data(DataType::Signature).as_bytes(), value.get_data(DataType::PqSignature).as_bytes()) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; };
if !connection.state().consume_challenge(AccountChallengeOperation::Delete, user_id, requester, nonce) { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; }
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; };
let Some(nonce) = value
.get_data(DataType::ServerNonce)
.as_signed_number()
.and_then(|v| u64::try_from(v).ok())
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
};
let (Some(signature), Some(pq_signature)) = (
value.get_data(DataType::Signature).as_bytes(),
value.get_data(DataType::PqSignature).as_bytes(),
) else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
};
if !connection.state().consume_challenge(
AccountChallengeOperation::Delete,
user_id,
requester,
nonce,
) {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
}
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
};
let payload = lifecycle_payload(b"tensamin:user-delete:v1\0", user_id, requester, nonce);
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err() || verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err() { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated).await; }
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err()
|| verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err()
{
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated)
.await;
}
complete_delete(connection, value, user.id).await
}
pub async fn erase_hosted_user_data_ack(connection: Arc<OmikronConnection>, value: CommunicationValue) -> OmikronResult<()> {
let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; };
pub async fn erase_hosted_user_data_ack(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId)
.await;
};
let iota_id = IotaId::from(value.get_sender() as i64);
match user_repo::acknowledge_pending_erasure(UserId::from(user_id), iota_id).await {
Ok(true) => connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())).await,
Ok(false) => connection.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated).await,
Err(_) => connection.send_error_response(value.get_id(), CommunicationType::ErrorInternal).await,
Ok(true) => {
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id()))
.await
}
Ok(false) => {
connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated)
.await
}
Err(_) => {
connection
.send_error_response(value.get_id(), CommunicationType::ErrorInternal)
.await
}
}
}