[Add] Better key rotation, more relays (Sealed Sender, not yet fully)
This commit is contained in:
parent
b38b68ad96
commit
d8ef068a48
9 changed files with 924 additions and 125 deletions
|
|
@ -227,6 +227,11 @@ pub fn apply_verified_relay_content(
|
||||||
let height = relay_number(&content.content, DataType::Height, &context.type_map)
|
let height = relay_number(&content.content, DataType::Height, &context.type_map)
|
||||||
.and_then(|value| i64::try_from(value).ok())
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let key_version =
|
||||||
|
relay_number(&content.content, DataType::VersionNumber, &context.type_map)
|
||||||
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.ok_or_else(|| "Relay MessageSend has an invalid VersionNumber".to_string())?;
|
||||||
let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map)
|
let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map)
|
||||||
.and_then(|value| i64::try_from(value).ok());
|
.and_then(|value| i64::try_from(value).ok());
|
||||||
if relay_string(
|
if relay_string(
|
||||||
|
|
@ -240,6 +245,22 @@ pub fn apply_verified_relay_content(
|
||||||
"Relay MessageSend identity does not match its protected message ID".into(),
|
"Relay MessageSend identity does not match its protected message ID".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let chat_id = if sender_id < recipient_id {
|
||||||
|
format!("{sender_id}:{recipient_id}")
|
||||||
|
} else {
|
||||||
|
format!("{recipient_id}:{sender_id}")
|
||||||
|
};
|
||||||
|
let latest_secret = e2ee_storage::get_chat_secret(ChatSecretQuery {
|
||||||
|
user_id: storage_owner.to_string(),
|
||||||
|
chat_id: chat_id.clone(),
|
||||||
|
secret_id: Some(format!("chat:{chat_id}:main")),
|
||||||
|
version: None,
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.ok_or_else(|| "Relay MessageSend has no committed chat secret".to_string())?;
|
||||||
|
if latest_secret.version != key_version {
|
||||||
|
return Err("Relay MessageSend uses a stale chat secret version".into());
|
||||||
|
}
|
||||||
chat_files::add_message(chat_files::NewMessage {
|
chat_files::add_message(chat_files::NewMessage {
|
||||||
relay_signer_id: sender_id,
|
relay_signer_id: sender_id,
|
||||||
relay_message_id: &context.message_id,
|
relay_message_id: &context.message_id,
|
||||||
|
|
@ -254,6 +275,7 @@ pub fn apply_verified_relay_content(
|
||||||
sent_by_self,
|
sent_by_self,
|
||||||
content: message,
|
content: message,
|
||||||
height,
|
height,
|
||||||
|
key_version,
|
||||||
reply_to,
|
reply_to,
|
||||||
origin_iota_received_at: sent_by_self.then_some(accepted_at),
|
origin_iota_received_at: sent_by_self.then_some(accepted_at),
|
||||||
destination_iota_received_at: (!sent_by_self).then_some(accepted_at),
|
destination_iota_received_at: (!sent_by_self).then_some(accepted_at),
|
||||||
|
|
@ -272,8 +294,29 @@ pub fn apply_verified_relay_content(
|
||||||
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
||||||
.and_then(|value| i64::try_from(value).ok())
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
.ok_or_else(|| "Relay MessageEdit is missing SendTime".to_string())?;
|
.ok_or_else(|| "Relay MessageEdit is missing SendTime".to_string())?;
|
||||||
chat_files::apply_remote_edit(recipient_id, sender_id, send_time, sender_id, message)
|
let external_user = if sent_by_self {
|
||||||
.map_err(|error| error.to_string())
|
recipient_id
|
||||||
|
} else {
|
||||||
|
sender_id
|
||||||
|
};
|
||||||
|
let result = if sent_by_self {
|
||||||
|
chat_files::edit_message(
|
||||||
|
storage_owner,
|
||||||
|
external_user,
|
||||||
|
send_time,
|
||||||
|
sender_id,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
chat_files::apply_remote_edit(
|
||||||
|
storage_owner,
|
||||||
|
external_user,
|
||||||
|
send_time,
|
||||||
|
sender_id,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
result.map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
"MessageReactionAdd" | "MessageReactionRemove" => {
|
"MessageReactionAdd" | "MessageReactionRemove" => {
|
||||||
let reaction = relay_string(&content.content, DataType::Reaction, &context.type_map)
|
let reaction = relay_string(&content.content, DataType::Reaction, &context.type_map)
|
||||||
|
|
@ -282,19 +325,45 @@ pub fn apply_verified_relay_content(
|
||||||
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
||||||
.and_then(|value| i64::try_from(value).ok())
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
.ok_or_else(|| "Relay reaction is missing SendTime".to_string())?;
|
.ok_or_else(|| "Relay reaction is missing SendTime".to_string())?;
|
||||||
let result = if content.message_type == "MessageReactionAdd" {
|
let external_user = if sent_by_self {
|
||||||
chat_files::add_reaction(recipient_id, sender_id, send_time, sender_id, reaction)
|
recipient_id
|
||||||
} else {
|
} else {
|
||||||
chat_files::remove_reaction(recipient_id, sender_id, send_time, sender_id, reaction)
|
sender_id
|
||||||
|
};
|
||||||
|
let result = if content.message_type == "MessageReactionAdd" {
|
||||||
|
chat_files::add_reaction(
|
||||||
|
storage_owner,
|
||||||
|
external_user,
|
||||||
|
send_time,
|
||||||
|
sender_id,
|
||||||
|
reaction,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
chat_files::remove_reaction(
|
||||||
|
storage_owner,
|
||||||
|
external_user,
|
||||||
|
send_time,
|
||||||
|
sender_id,
|
||||||
|
reaction,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
result.map_err(|error| error.to_string())
|
result.map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
"MessageDeleteLive" => {
|
"MessageDelete" | "MessageDeleteLive" => {
|
||||||
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
||||||
.and_then(|value| i64::try_from(value).ok())
|
.and_then(|value| i64::try_from(value).ok())
|
||||||
.ok_or_else(|| "Relay MessageDeleteLive is missing SendTime".to_string())?;
|
.ok_or_else(|| "Relay MessageDeleteLive is missing SendTime".to_string())?;
|
||||||
chat_files::apply_remote_delete(recipient_id, sender_id, send_time, sender_id)
|
let external_user = if sent_by_self {
|
||||||
.map_err(|error| error.to_string())
|
recipient_id
|
||||||
|
} else {
|
||||||
|
sender_id
|
||||||
|
};
|
||||||
|
let result = if sent_by_self {
|
||||||
|
chat_files::delete_message(storage_owner, external_user, send_time)
|
||||||
|
} else {
|
||||||
|
chat_files::apply_remote_delete(storage_owner, external_user, send_time, sender_id)
|
||||||
|
};
|
||||||
|
result.map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
"SetChatSecret" => {
|
"SetChatSecret" => {
|
||||||
let frame = CommunicationValue::new(CommunicationType::SetChatSecret)
|
let frame = CommunicationValue::new(CommunicationType::SetChatSecret)
|
||||||
|
|
@ -302,7 +371,7 @@ pub fn apply_verified_relay_content(
|
||||||
let recipients = chat_secret_recipients(&frame)
|
let recipients = chat_secret_recipients(&frame)
|
||||||
.ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?;
|
.ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?;
|
||||||
let recipient = recipients
|
let recipient = recipients
|
||||||
.into_iter()
|
.iter()
|
||||||
.find(|value| value.user_id == storage_owner.to_string())
|
.find(|value| value.user_id == storage_owner.to_string())
|
||||||
.ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?;
|
.ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?;
|
||||||
let chat_id = data_string(&frame, DataType::ChatId)
|
let chat_id = data_string(&frame, DataType::ChatId)
|
||||||
|
|
@ -310,16 +379,62 @@ pub fn apply_verified_relay_content(
|
||||||
let secret_id = data_string(&frame, DataType::SecretId)
|
let secret_id = data_string(&frame, DataType::SecretId)
|
||||||
.ok_or_else(|| "Relay SetChatSecret is missing SecretId".to_string())?;
|
.ok_or_else(|| "Relay SetChatSecret is missing SecretId".to_string())?;
|
||||||
let version = data_i64(&frame, DataType::VersionNumber)
|
let version = data_i64(&frame, DataType::VersionNumber)
|
||||||
|
.filter(|value| *value > 0)
|
||||||
.ok_or_else(|| "Relay SetChatSecret is missing VersionNumber".to_string())?;
|
.ok_or_else(|| "Relay SetChatSecret is missing VersionNumber".to_string())?;
|
||||||
let wrapping_scheme = data_string(&frame, DataType::WrappingScheme)
|
let wrapping_scheme = data_string(&frame, DataType::WrappingScheme)
|
||||||
.ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?;
|
.ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?;
|
||||||
|
let mut participants = chat_id
|
||||||
|
.split(':')
|
||||||
|
.map(|value| value.parse::<u64>())
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| "Relay SetChatSecret ChatId is not a user pair".to_string())?;
|
||||||
|
if participants.len() != 2
|
||||||
|
|| participants.contains(&0)
|
||||||
|
|| participants[0] == participants[1]
|
||||||
|
{
|
||||||
|
return Err("Relay SetChatSecret ChatId is not a user pair".into());
|
||||||
|
}
|
||||||
|
participants.sort_unstable();
|
||||||
|
if chat_id != format!("{}:{}", participants[0], participants[1]) {
|
||||||
|
return Err("Relay SetChatSecret ChatId is not canonical".into());
|
||||||
|
}
|
||||||
|
if !participants.contains(&context.signer_id) {
|
||||||
|
return Err("Relay SetChatSecret signer is not in ChatId".into());
|
||||||
|
}
|
||||||
|
if !participants.contains(&context.final_recipient_id) {
|
||||||
|
return Err("Relay SetChatSecret recipient is not in ChatId".into());
|
||||||
|
}
|
||||||
|
if secret_id != format!("chat:{chat_id}:main") {
|
||||||
|
return Err("Relay SetChatSecret has an invalid secret namespace".into());
|
||||||
|
}
|
||||||
|
if context.signer_id != participants[0] {
|
||||||
|
return Err(
|
||||||
|
"Relay SetChatSecret was signed by a non-authoritative participant".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if context.final_recipient_id != participants[1] {
|
||||||
|
return Err("Relay SetChatSecret recipient is not the peer participant".into());
|
||||||
|
}
|
||||||
|
if recipients.len() != 2 {
|
||||||
|
return Err("Relay SetChatSecret must contain exactly two recipients".into());
|
||||||
|
}
|
||||||
|
let mut recipient_ids = recipients
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.user_id.parse::<u64>())
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| "Relay SetChatSecret has invalid recipient IDs".to_string())?;
|
||||||
|
recipient_ids.sort_unstable();
|
||||||
|
let expected_recipient_ids = participants.clone();
|
||||||
|
if recipient_ids != expected_recipient_ids {
|
||||||
|
return Err("Relay SetChatSecret recipients do not match relay identities".into());
|
||||||
|
}
|
||||||
e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret {
|
e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret {
|
||||||
user_id: storage_owner.to_string(),
|
user_id: storage_owner.to_string(),
|
||||||
chat_id,
|
chat_id,
|
||||||
secret_id,
|
secret_id,
|
||||||
version,
|
version,
|
||||||
encrypted_secret: recipient.encrypted_secret,
|
encrypted_secret: recipient.encrypted_secret.clone(),
|
||||||
kem_ciphertext: recipient.kem_ciphertext,
|
kem_ciphertext: recipient.kem_ciphertext.clone(),
|
||||||
wrapping_scheme,
|
wrapping_scheme,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at: now_millis_i64(),
|
updated_at: now_millis_i64(),
|
||||||
|
|
@ -338,9 +453,20 @@ pub fn apply_verified_relay_content(
|
||||||
if user_id <= 0 {
|
if user_id <= 0 {
|
||||||
return Err("Relay AddConversation has an invalid storage owner".into());
|
return Err("Relay AddConversation has an invalid storage owner".into());
|
||||||
}
|
}
|
||||||
|
if sender_id == recipient_id {
|
||||||
|
return Err("Relay AddConversation does not support self conversations".into());
|
||||||
|
}
|
||||||
|
if other_id != sender_id && other_id != recipient_id {
|
||||||
|
return Err("Relay AddConversation does not match relay identities".into());
|
||||||
|
}
|
||||||
|
let expected_partner = if sent_by_self {
|
||||||
|
recipient_id
|
||||||
|
} else {
|
||||||
|
sender_id
|
||||||
|
};
|
||||||
add_conversation_for_user(
|
add_conversation_for_user(
|
||||||
user_id,
|
user_id,
|
||||||
other_id,
|
expected_partner,
|
||||||
relay_string(
|
relay_string(
|
||||||
&content.content,
|
&content.content,
|
||||||
DataType::ChatPartnerName,
|
DataType::ChatPartnerName,
|
||||||
|
|
@ -474,13 +600,22 @@ fn stored_message_fields(
|
||||||
DataType::Height,
|
DataType::Height,
|
||||||
DataValue::SignedNumber(message.height as i128),
|
DataValue::SignedNumber(message.height as i128),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
DataType::VersionNumber,
|
||||||
|
DataValue::SignedNumber(message.key_version as i128),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
let sender_id = if message.sent_by_self {
|
let sender_id = if message.sent_by_self {
|
||||||
storage_owner
|
storage_owner
|
||||||
} else {
|
} else {
|
||||||
partner_id
|
partner_id
|
||||||
};
|
};
|
||||||
if let Ok(sender_id) = u128::try_from(sender_id) {
|
if let Some(relay_signer_id) = message.relay_signer_id {
|
||||||
|
fields.push((
|
||||||
|
DataType::SenderId,
|
||||||
|
DataValue::SignedNumber(relay_signer_id.into()),
|
||||||
|
));
|
||||||
|
} else if let Ok(sender_id) = u128::try_from(sender_id) {
|
||||||
fields.push((DataType::SenderId, DataValue::UnsignedNumber(sender_id)));
|
fields.push((DataType::SenderId, DataValue::UnsignedNumber(sender_id)));
|
||||||
}
|
}
|
||||||
if let Some(relay_message_id) = &message.relay_message_id {
|
if let Some(relay_message_id) = &message.relay_message_id {
|
||||||
|
|
@ -489,12 +624,6 @@ fn stored_message_fields(
|
||||||
DataValue::Str(relay_message_id.clone()),
|
DataValue::Str(relay_message_id.clone()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(relay_signer_id) = message.relay_signer_id {
|
|
||||||
fields.push((
|
|
||||||
DataType::SenderId,
|
|
||||||
DataValue::SignedNumber(relay_signer_id.into()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for (data_type, timestamp) in [
|
for (data_type, timestamp) in [
|
||||||
(DataType::AuthoredAt, message.authored_at),
|
(DataType::AuthoredAt, message.authored_at),
|
||||||
(
|
(
|
||||||
|
|
@ -607,6 +736,7 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue {
|
||||||
user_id,
|
user_id,
|
||||||
chat_id,
|
chat_id,
|
||||||
secret_id: data_string(cv, DataType::SecretId),
|
secret_id: data_string(cv, DataType::SecretId),
|
||||||
|
version: data_i64(cv, DataType::VersionNumber),
|
||||||
}) {
|
}) {
|
||||||
Ok(Some(record)) => CommunicationValue::new(CommunicationType::ChatSecretResponse)
|
Ok(Some(record)) => CommunicationValue::new(CommunicationType::ChatSecretResponse)
|
||||||
.with_request_id(cv)
|
.with_request_id(cv)
|
||||||
|
|
@ -2326,6 +2456,46 @@ pub fn handle_user_block_check(cv: &CommunicationValue) -> CommunicationValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod stored_message_tests {
|
||||||
|
use super::stored_message_value;
|
||||||
|
use iota_storage::util::chat_files::StoredMessage;
|
||||||
|
use mtp::codec::DataValue;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relay_message_value_has_a_single_sender_id() {
|
||||||
|
let message = StoredMessage {
|
||||||
|
id: 1,
|
||||||
|
external_user: 9,
|
||||||
|
relay_signer_id: Some(9),
|
||||||
|
relay_message_id: Some("relay-1".to_string()),
|
||||||
|
message_time: 2,
|
||||||
|
authored_at: None,
|
||||||
|
origin_iota_received_at: None,
|
||||||
|
destination_iota_received_at: None,
|
||||||
|
client_received_at: None,
|
||||||
|
client_received_recorded_at: None,
|
||||||
|
read_at: None,
|
||||||
|
read_recorded_at: None,
|
||||||
|
delivery_failed_at: None,
|
||||||
|
delivery_failure: None,
|
||||||
|
content: "message".to_string(),
|
||||||
|
edited: false,
|
||||||
|
sent_by_self: false,
|
||||||
|
message_state: "sent".to_string(),
|
||||||
|
height: 1,
|
||||||
|
key_version: 1,
|
||||||
|
reply_to: None,
|
||||||
|
reactions: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = stored_message_value(&message, 7, 9);
|
||||||
|
|
||||||
|
assert!(matches!(value, DataValue::Container(_)));
|
||||||
|
assert!(value.to_bytes().is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod synced_settings_tests {
|
mod synced_settings_tests {
|
||||||
use super::{handle_synced_setting_get, handle_synced_setting_set, parse_setting_locator};
|
use super::{handle_synced_setting_get, handle_synced_setting_set, parse_setting_locator};
|
||||||
|
|
|
||||||
|
|
@ -212,13 +212,12 @@ where
|
||||||
pub fn open_verified_relay_content(
|
pub fn open_verified_relay_content(
|
||||||
relay: &VerifiedRelay,
|
relay: &VerifiedRelay,
|
||||||
keyrings: &[&Keyring],
|
keyrings: &[&Keyring],
|
||||||
expected_recipient_id: u64,
|
|
||||||
) -> Result<VerifiedRelayContent, RelayValidationError> {
|
) -> Result<VerifiedRelayContent, RelayValidationError> {
|
||||||
Ok(open_relay_content_with_limits_without_replay(
|
Ok(open_relay_content_with_limits_without_replay(
|
||||||
&relay.metadata,
|
&relay.metadata,
|
||||||
keyrings,
|
keyrings,
|
||||||
&relay.signing_keys,
|
&relay.signing_keys,
|
||||||
Some(expected_recipient_id),
|
Some(relay.context.final_recipient_id),
|
||||||
RelayOpenOptions {
|
RelayOpenOptions {
|
||||||
policy: RELAY_PROTECTION_POLICY,
|
policy: RELAY_PROTECTION_POLICY,
|
||||||
decode_limits: relay.metadata.decode_limits(),
|
decode_limits: relay.metadata.decode_limits(),
|
||||||
|
|
@ -290,6 +289,24 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn content_validation_uses_final_recipient_id() -> Result<(), String> {
|
||||||
|
let (signer, recipient, frame) = relay("final-recipient")?;
|
||||||
|
let trusted_key = signer.public_key_bundle();
|
||||||
|
let verified = verify_relay_metadata(&frame, 99, &recipient, move |signer_id| async move {
|
||||||
|
(signer_id == 7)
|
||||||
|
.then_some(vec![trusted_key])
|
||||||
|
.ok_or(RelayValidationError::MissingSigningKeys(signer_id))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let content = open_verified_relay_content(&verified, &[&recipient])
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
assert_eq!(content.final_recipient_id, 42);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_metadata_signed_by_untrusted_key() -> Result<(), String> {
|
async fn rejects_metadata_signed_by_untrusted_key() -> Result<(), String> {
|
||||||
let (_signer, recipient, frame) = relay("wrong-key")?;
|
let (_signer, recipient, frame) = relay("wrong-key")?;
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,10 @@ pub fn finalize_local_release(
|
||||||
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])?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE pending_relays SET delivery_state = 'waiting_client', last_error = 'user_released' WHERE target_kind = 0 AND destination_id = ?1",
|
||||||
|
params![user_id],
|
||||||
|
)?;
|
||||||
tx.execute(
|
tx.execute(
|
||||||
r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at)
|
r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at)
|
||||||
VALUES (?1, ?2, 'released', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3)
|
VALUES (?1, ?2, 'released', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3)
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ pub struct StoredMessage {
|
||||||
pub sent_by_self: bool,
|
pub sent_by_self: bool,
|
||||||
pub message_state: String,
|
pub message_state: String,
|
||||||
pub height: i64,
|
pub height: i64,
|
||||||
|
pub key_version: i64,
|
||||||
pub reply_to: Option<i64>,
|
pub reply_to: Option<i64>,
|
||||||
pub reactions: Vec<StoredReaction>,
|
pub reactions: Vec<StoredReaction>,
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +83,7 @@ pub struct NewMessage<'a> {
|
||||||
pub sent_by_self: bool,
|
pub sent_by_self: bool,
|
||||||
pub content: &'a str,
|
pub content: &'a str,
|
||||||
pub height: i64,
|
pub height: i64,
|
||||||
|
pub key_version: i64,
|
||||||
pub reply_to: Option<i64>,
|
pub reply_to: Option<i64>,
|
||||||
pub origin_iota_received_at: Option<i64>,
|
pub origin_iota_received_at: Option<i64>,
|
||||||
pub destination_iota_received_at: Option<i64>,
|
pub destination_iota_received_at: Option<i64>,
|
||||||
|
|
@ -596,6 +598,7 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
|
||||||
sent_by_self,
|
sent_by_self,
|
||||||
content,
|
content,
|
||||||
height,
|
height,
|
||||||
|
key_version,
|
||||||
reply_to,
|
reply_to,
|
||||||
origin_iota_received_at,
|
origin_iota_received_at,
|
||||||
destination_iota_received_at,
|
destination_iota_received_at,
|
||||||
|
|
@ -617,9 +620,9 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO messages (
|
INSERT INTO messages (
|
||||||
storage_owner, external_user, message_time, content, sent_by_self,
|
storage_owner, external_user, message_time, content, sent_by_self,
|
||||||
message_state, height, reply_to, relay_signer_id, relay_message_id,
|
message_state, height, key_version, reply_to, relay_signer_id, relay_message_id,
|
||||||
authored_at, origin_iota_received_at, destination_iota_received_at, stored_at, expires_at
|
authored_at, origin_iota_received_at, destination_iota_received_at, stored_at, expires_at
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
|
||||||
"#,
|
"#,
|
||||||
params![
|
params![
|
||||||
storage_owner,
|
storage_owner,
|
||||||
|
|
@ -629,6 +632,7 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
|
||||||
i64::from(sent_by_self),
|
i64::from(sent_by_self),
|
||||||
initial_state.as_str(),
|
initial_state.as_str(),
|
||||||
height,
|
height,
|
||||||
|
key_version,
|
||||||
reply_to,
|
reply_to,
|
||||||
relay_signer_id,
|
relay_signer_id,
|
||||||
relay_message_id,
|
relay_message_id,
|
||||||
|
|
@ -960,7 +964,7 @@ pub fn get_messages(
|
||||||
SELECT id, relay_signer_id, relay_message_id, message_time, authored_at,
|
SELECT id, relay_signer_id, relay_message_id, message_time, authored_at,
|
||||||
origin_iota_received_at, destination_iota_received_at,
|
origin_iota_received_at, destination_iota_received_at,
|
||||||
client_received_at, client_received_recorded_at, read_at,
|
client_received_at, client_received_recorded_at, read_at,
|
||||||
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height,
|
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version,
|
||||||
reply_to, edited_count
|
reply_to, edited_count
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 AND history_deleted = 0
|
WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 AND history_deleted = 0
|
||||||
|
|
@ -991,8 +995,9 @@ pub fn get_messages(
|
||||||
sent_by_self: row.get::<_, i64>(14)? != 0,
|
sent_by_self: row.get::<_, i64>(14)? != 0,
|
||||||
message_state: row.get(15)?,
|
message_state: row.get(15)?,
|
||||||
height: row.get(16).unwrap_or(0),
|
height: row.get(16).unwrap_or(0),
|
||||||
reply_to: row.get(17).ok().flatten(),
|
key_version: row.get(17).unwrap_or(1),
|
||||||
edited: row.get::<_, i64>(18).unwrap_or(0) > 0,
|
reply_to: row.get(18).ok().flatten(),
|
||||||
|
edited: row.get::<_, i64>(19).unwrap_or(0) > 0,
|
||||||
reactions: Vec::new(),
|
reactions: Vec::new(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -1033,7 +1038,7 @@ pub fn get_message(
|
||||||
SELECT id, relay_signer_id, relay_message_id, message_time, authored_at,
|
SELECT id, relay_signer_id, relay_message_id, message_time, authored_at,
|
||||||
origin_iota_received_at, destination_iota_received_at,
|
origin_iota_received_at, destination_iota_received_at,
|
||||||
client_received_at, client_received_recorded_at, read_at,
|
client_received_at, client_received_recorded_at, read_at,
|
||||||
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height,
|
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version,
|
||||||
reply_to, edited_count, external_user
|
reply_to, edited_count, external_user
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE storage_owner = ?1
|
WHERE storage_owner = ?1
|
||||||
|
|
@ -1064,9 +1069,10 @@ pub fn get_message(
|
||||||
sent_by_self: row.get::<_, i64>(14)? != 0,
|
sent_by_self: row.get::<_, i64>(14)? != 0,
|
||||||
message_state: row.get(15)?,
|
message_state: row.get(15)?,
|
||||||
height: row.get(16).unwrap_or(0),
|
height: row.get(16).unwrap_or(0),
|
||||||
reply_to: row.get(17).ok().flatten(),
|
key_version: row.get(17).unwrap_or(1),
|
||||||
edited: row.get::<_, i64>(18).unwrap_or(0) > 0,
|
reply_to: row.get(18).ok().flatten(),
|
||||||
external_user: row.get(19)?,
|
edited: row.get::<_, i64>(19).unwrap_or(0) > 0,
|
||||||
|
external_user: row.get(20)?,
|
||||||
reactions: Vec::new(),
|
reactions: Vec::new(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -1138,9 +1144,9 @@ pub fn get_messages_by_ids(storage_owner: i64, ids: &[i64]) -> Vec<StoredMessage
|
||||||
// A journal id uniquely identifies a row. Load all messages for this owner and retain only
|
// A journal id uniquely identifies a row. Load all messages for this owner and retain only
|
||||||
// those ids; this keeps reaction hydration identical to normal message loading.
|
// those ids; this keeps reaction hydration identical to normal message loading.
|
||||||
match db::with_db(|conn| {
|
match db::with_db(|conn| {
|
||||||
let mut stmt = conn.prepare("SELECT id, relay_signer_id, relay_message_id, message_time, authored_at, origin_iota_received_at, destination_iota_received_at, client_received_at, client_received_recorded_at, read_at, read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, reply_to, edited_count, external_user FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0 AND history_deleted = 0")?;
|
let mut stmt = conn.prepare("SELECT id, relay_signer_id, relay_message_id, message_time, authored_at, origin_iota_received_at, destination_iota_received_at, client_received_at, client_received_recorded_at, read_at, read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version, reply_to, edited_count, external_user FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0 AND history_deleted = 0")?;
|
||||||
let rows = stmt.query_map([storage_owner], |row| {
|
let rows = stmt.query_map([storage_owner], |row| {
|
||||||
let external_user: i64 = row.get(19)?;
|
let external_user: i64 = row.get(20)?;
|
||||||
Ok(StoredMessage {
|
Ok(StoredMessage {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
external_user,
|
external_user,
|
||||||
|
|
@ -1160,8 +1166,9 @@ pub fn get_messages_by_ids(storage_owner: i64, ids: &[i64]) -> Vec<StoredMessage
|
||||||
sent_by_self: row.get::<_, i64>(14)? != 0,
|
sent_by_self: row.get::<_, i64>(14)? != 0,
|
||||||
message_state: row.get(15)?,
|
message_state: row.get(15)?,
|
||||||
height: row.get(16).unwrap_or(0),
|
height: row.get(16).unwrap_or(0),
|
||||||
reply_to: row.get(17).ok().flatten(),
|
key_version: row.get(17).unwrap_or(1),
|
||||||
edited: row.get::<_, i64>(18).unwrap_or(0) > 0,
|
reply_to: row.get(18).ok().flatten(),
|
||||||
|
edited: row.get::<_, i64>(19).unwrap_or(0) > 0,
|
||||||
reactions: Vec::new(),
|
reactions: Vec::new(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,21 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let messages_exist: bool = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages')",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if messages_exist {
|
||||||
|
add_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"key_version",
|
||||||
|
"key_version INTEGER NOT NULL DEFAULT 1",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
if current_version < 5 {
|
if current_version < 5 {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -814,6 +829,35 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if current_version < 24 {
|
||||||
|
let pending_relays_exist: bool = conn.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'pending_relays')",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
if pending_relays_exist {
|
||||||
|
add_table_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"pending_relays",
|
||||||
|
"delivery_state",
|
||||||
|
"delivery_state TEXT NOT NULL DEFAULT 'ready' CHECK (delivery_state IN ('ready', 'waiting_client', 'quarantined'))",
|
||||||
|
)?;
|
||||||
|
add_table_column_if_missing(conn, "pending_relays", "last_error", "last_error TEXT")?;
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
UPDATE pending_relays
|
||||||
|
SET delivery_state = 'quarantined', last_error = 'target_recipient_mismatch'
|
||||||
|
WHERE target_kind = 0
|
||||||
|
AND relay_destination_user_id IS NOT NULL
|
||||||
|
AND destination_id <> relay_destination_user_id;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pending_relays_active
|
||||||
|
ON pending_relays (delivery_state, target_kind, destination_id, id);
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
conn.pragma_update(None, "user_version", 24)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -883,7 +927,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, 23);
|
assert_eq!(version, 24);
|
||||||
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")?;
|
||||||
|
|
@ -902,7 +946,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, 23);
|
assert_eq!(version, 24);
|
||||||
for table in [
|
for table in [
|
||||||
"sync_heads",
|
"sync_heads",
|
||||||
"sync_events",
|
"sync_events",
|
||||||
|
|
@ -926,7 +970,13 @@ mod tests {
|
||||||
)?;
|
)?;
|
||||||
assert_eq!(exists, 1);
|
assert_eq!(exists, 1);
|
||||||
}
|
}
|
||||||
for column in ["frame_id", "target_kind", "type_map_version"] {
|
for column in [
|
||||||
|
"frame_id",
|
||||||
|
"target_kind",
|
||||||
|
"type_map_version",
|
||||||
|
"delivery_state",
|
||||||
|
"last_error",
|
||||||
|
] {
|
||||||
let mut statement =
|
let mut statement =
|
||||||
conn.prepare("SELECT 1 FROM pragma_table_info('pending_relays') WHERE name = ?1")?;
|
conn.prepare("SELECT 1 FROM pragma_table_info('pending_relays') WHERE name = ?1")?;
|
||||||
assert!(statement.exists([column])?);
|
assert!(statement.exists([column])?);
|
||||||
|
|
@ -942,7 +992,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, 23);
|
assert_eq!(version, 24);
|
||||||
for column in [
|
for column in [
|
||||||
"id",
|
"id",
|
||||||
"user_id",
|
"user_id",
|
||||||
|
|
@ -1037,7 +1087,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, 23);
|
assert_eq!(version, 24);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ pub struct ChatSecretQuery {
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub chat_id: String,
|
pub chat_id: String,
|
||||||
pub secret_id: Option<String>,
|
pub secret_id: Option<String>,
|
||||||
|
pub version: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
||||||
db::create_shared_connection(
|
let database = db::create_shared_connection(
|
||||||
"e2ee",
|
"e2ee",
|
||||||
r#"
|
r#"
|
||||||
PRAGMA journal_mode = WAL;
|
PRAGMA journal_mode = WAL;
|
||||||
|
|
@ -44,7 +45,7 @@ static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
||||||
wrapping_scheme TEXT NOT NULL,
|
wrapping_scheme TEXT NOT NULL,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL,
|
updated_at INTEGER NOT NULL,
|
||||||
PRIMARY KEY (user_id, chat_id, secret_id)
|
PRIMARY KEY (user_id, chat_id, secret_id, version)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_secrets_owner
|
CREATE INDEX IF NOT EXISTS idx_chat_secrets_owner
|
||||||
|
|
@ -52,37 +53,81 @@ static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
||||||
|
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.expect("Failed to create or initialize E2EE DB")
|
.expect("Failed to create or initialize E2EE DB");
|
||||||
|
if let Ok(connection) = database.lock() {
|
||||||
|
let legacy = connection
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM pragma_table_info('chat_secrets') WHERE name = 'version' AND pk = 4",
|
||||||
|
[],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)
|
||||||
|
.unwrap_or(0)
|
||||||
|
== 0;
|
||||||
|
if legacy {
|
||||||
|
connection
|
||||||
|
.execute_batch(
|
||||||
|
"ALTER TABLE chat_secrets RENAME TO chat_secrets_legacy;
|
||||||
|
CREATE TABLE chat_secrets (
|
||||||
|
user_id TEXT NOT NULL, chat_id TEXT NOT NULL,
|
||||||
|
secret_id TEXT NOT NULL, version INTEGER NOT NULL,
|
||||||
|
encrypted_secret BLOB NOT NULL, kem_ciphertext BLOB NOT NULL,
|
||||||
|
wrapping_scheme TEXT NOT NULL, created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, chat_id, secret_id, version)
|
||||||
|
);
|
||||||
|
INSERT INTO chat_secrets SELECT * FROM chat_secrets_legacy;
|
||||||
|
DROP TABLE chat_secrets_legacy;",
|
||||||
|
)
|
||||||
|
.expect("Failed to migrate E2EE secret history schema");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
database
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn put_chat_secret(record: StoredChatSecret) -> Result<(), StorageError> {
|
pub fn put_chat_secret(record: StoredChatSecret) -> Result<(), StorageError> {
|
||||||
db::with_conn(&E2EE_DB, |conn| {
|
db::with_conn(&E2EE_DB, |conn| {
|
||||||
conn.execute(
|
let tx = conn.unchecked_transaction()?;
|
||||||
r#"
|
let existing = tx.query_row(
|
||||||
INSERT INTO chat_secrets (
|
"SELECT encrypted_secret, kem_ciphertext, wrapping_scheme FROM chat_secrets WHERE user_id = ?1 AND chat_id = ?2 AND secret_id = ?3 AND version = ?4",
|
||||||
user_id, chat_id, secret_id, version, encrypted_secret,
|
params![&record.user_id, &record.chat_id, &record.secret_id, record.version],
|
||||||
kem_ciphertext, wrapping_scheme, created_at, updated_at
|
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?, row.get::<_, String>(2)?)),
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
).optional()?;
|
||||||
ON CONFLICT(user_id, chat_id, secret_id) DO UPDATE SET
|
if let Some(existing) = existing {
|
||||||
version = excluded.version,
|
if existing
|
||||||
encrypted_secret = excluded.encrypted_secret,
|
!= (
|
||||||
kem_ciphertext = excluded.kem_ciphertext,
|
record.encrypted_secret.clone(),
|
||||||
wrapping_scheme = excluded.wrapping_scheme,
|
record.kem_ciphertext.clone(),
|
||||||
created_at = excluded.created_at,
|
record.wrapping_scheme.clone(),
|
||||||
updated_at = excluded.updated_at
|
)
|
||||||
"#,
|
{
|
||||||
params![
|
return Err(rusqlite::Error::InvalidParameterName(
|
||||||
record.user_id,
|
"immutable chat secret version conflict".into(),
|
||||||
record.chat_id,
|
));
|
||||||
record.secret_id,
|
}
|
||||||
record.version,
|
return Ok(());
|
||||||
record.encrypted_secret,
|
}
|
||||||
record.kem_ciphertext,
|
let latest = tx.query_row(
|
||||||
record.wrapping_scheme,
|
"SELECT MAX(version) FROM chat_secrets WHERE user_id = ?1 AND chat_id = ?2 AND secret_id = ?3",
|
||||||
record.created_at,
|
params![&record.user_id, &record.chat_id, &record.secret_id],
|
||||||
record.updated_at,
|
|row| row.get::<_, Option<i64>>(0),
|
||||||
],
|
|
||||||
)?;
|
)?;
|
||||||
|
let expected = match latest {
|
||||||
|
None => 1,
|
||||||
|
Some(version) => version.checked_add(1).ok_or_else(|| {
|
||||||
|
rusqlite::Error::InvalidParameterName("chat secret version overflow".into())
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
if record.version != expected {
|
||||||
|
return Err(rusqlite::Error::InvalidParameterName(format!(
|
||||||
|
"chat secret version is out of sequence: expected {expected}, got {}",
|
||||||
|
record.version
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO chat_secrets (user_id, chat_id, secret_id, version, encrypted_secret, kem_ciphertext, wrapping_scheme, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||||
|
params![record.user_id, record.chat_id, record.secret_id, record.version, record.encrypted_secret, record.kem_ciphertext, record.wrapping_scheme, record.created_at, record.updated_at],
|
||||||
|
)?;
|
||||||
|
tx.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -117,10 +162,11 @@ pub fn get_chat_secret(query: ChatSecretQuery) -> Result<Option<StoredChatSecret
|
||||||
WHERE user_id = ?1
|
WHERE user_id = ?1
|
||||||
AND chat_id = ?2
|
AND chat_id = ?2
|
||||||
AND (?3 IS NULL OR secret_id = ?3)
|
AND (?3 IS NULL OR secret_id = ?3)
|
||||||
ORDER BY updated_at DESC
|
AND (?4 IS NULL OR version = ?4)
|
||||||
|
ORDER BY version DESC, updated_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#,
|
"#,
|
||||||
params![query.user_id, query.chat_id, query.secret_id],
|
params![query.user_id, query.chat_id, query.secret_id, query.version],
|
||||||
chat_secret_from_row,
|
chat_secret_from_row,
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,26 @@ pub struct PendingRelay {
|
||||||
pub relay_signer_id: Option<i64>,
|
pub relay_signer_id: Option<i64>,
|
||||||
pub relay_destination_user_id: Option<i64>,
|
pub relay_destination_user_id: Option<i64>,
|
||||||
pub relay_message_id: Option<String>,
|
pub relay_message_id: Option<String>,
|
||||||
|
pub delivery_state: PendingRelayDeliveryState,
|
||||||
|
pub last_error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PendingRelayDeliveryState {
|
||||||
|
Ready,
|
||||||
|
WaitingClient,
|
||||||
|
Quarantined,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PendingRelayDeliveryState {
|
||||||
|
fn from_db(value: String) -> Result<Self, rusqlite::Error> {
|
||||||
|
match value.as_str() {
|
||||||
|
"ready" => Ok(Self::Ready),
|
||||||
|
"waiting_client" => Ok(Self::WaitingClient),
|
||||||
|
"quarantined" => Ok(Self::Quarantined),
|
||||||
|
_ => Err(rusqlite::Error::InvalidQuery),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -31,6 +51,15 @@ pub fn enqueue(
|
||||||
frame_id: u32,
|
frame_id: u32,
|
||||||
type_map_version: &str,
|
type_map_version: &str,
|
||||||
) -> Result<(), StorageError> {
|
) -> Result<(), StorageError> {
|
||||||
|
if let RouteTarget::User(destination) = target {
|
||||||
|
let destination = i64::try_from(destination)
|
||||||
|
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
|
||||||
|
if destination != relay.destination_user_id {
|
||||||
|
return Err(StorageError::Other(
|
||||||
|
"user relay target does not match final recipient".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
db::with_immediate_transaction(|tx| {
|
db::with_immediate_transaction(|tx| {
|
||||||
enqueue_in_tx(
|
enqueue_in_tx(
|
||||||
tx,
|
tx,
|
||||||
|
|
@ -79,7 +108,7 @@ pub fn enqueue_in_tx(
|
||||||
pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
|
pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
|
||||||
db::with_db(|connection| {
|
db::with_db(|connection| {
|
||||||
let mut statement = connection.prepare(
|
let mut statement = connection.prepare(
|
||||||
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id FROM pending_relays ORDER BY id LIMIT ?1",
|
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error FROM pending_relays ORDER BY id LIMIT ?1",
|
||||||
)?;
|
)?;
|
||||||
let rows = statement.query_map(params![limit.clamp(1, 500)], |row| {
|
let rows = statement.query_map(params![limit.clamp(1, 500)], |row| {
|
||||||
let destination_id = row.get::<_, i64>(1)?;
|
let destination_id = row.get::<_, i64>(1)?;
|
||||||
|
|
@ -118,12 +147,26 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
|
||||||
relay_signer_id: row.get(7)?,
|
relay_signer_id: row.get(7)?,
|
||||||
relay_destination_user_id: row.get(8)?,
|
relay_destination_user_id: row.get(8)?,
|
||||||
relay_message_id: row.get(9)?,
|
relay_message_id: row.get(9)?,
|
||||||
|
delivery_state: PendingRelayDeliveryState::from_db(row.get(10)?)?,
|
||||||
|
last_error: row.get(11)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
|
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Select only relays whose target and signer remain locally managed. This is
|
||||||
|
* the delivery-authority check after user release, independent of row state. */
|
||||||
|
pub fn list_active(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
|
||||||
|
db::with_db(|connection| {
|
||||||
|
let mut statement = connection.prepare(
|
||||||
|
"SELECT p.id, p.destination_id, p.target_kind, p.frame, p.created_at, p.frame_id, p.type_map_version, p.relay_signer_id, p.relay_destination_user_id, p.relay_message_id, p.delivery_state, p.last_error FROM pending_relays p WHERE p.delivery_state = 'ready' AND ((p.target_kind = 0 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.destination_id)) OR (p.target_kind = 1 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.relay_signer_id))) ORDER BY p.id LIMIT ?1",
|
||||||
|
)?;
|
||||||
|
let rows = statement.query_map(params![limit.clamp(1, 500)], pending_relay_from_row)?;
|
||||||
|
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_without_relay_identity() -> Result<Vec<PendingRelay>, StorageError> {
|
pub fn list_without_relay_identity() -> Result<Vec<PendingRelay>, StorageError> {
|
||||||
list_without_relay_identity_after(0, i64::MAX)
|
list_without_relay_identity_after(0, i64::MAX)
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +176,7 @@ pub fn list_without_relay_identity_after(
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<PendingRelay>, StorageError> {
|
) -> Result<Vec<PendingRelay>, StorageError> {
|
||||||
db::with_db(|connection| {
|
db::with_db(|connection| {
|
||||||
let mut statement = connection.prepare("SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id FROM pending_relays WHERE id > ?1 AND (relay_signer_id IS NULL OR relay_destination_user_id IS NULL OR relay_message_id IS NULL) ORDER BY id LIMIT ?2")?;
|
let mut statement = connection.prepare("SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error FROM pending_relays WHERE id > ?1 AND (relay_signer_id IS NULL OR relay_destination_user_id IS NULL OR relay_message_id IS NULL) ORDER BY id LIMIT ?2")?;
|
||||||
let rows = statement.query_map(
|
let rows = statement.query_map(
|
||||||
params![after_id, limit.clamp(1, 500)],
|
params![after_id, limit.clamp(1, 500)],
|
||||||
pending_relay_from_row,
|
pending_relay_from_row,
|
||||||
|
|
@ -161,11 +204,68 @@ fn pending_relay_from_row(row: &rusqlite::Row<'_>) -> Result<PendingRelay, rusql
|
||||||
relay_signer_id: row.get(7)?,
|
relay_signer_id: row.get(7)?,
|
||||||
relay_destination_user_id: row.get(8)?,
|
relay_destination_user_id: row.get(8)?,
|
||||||
relay_message_id: row.get(9)?,
|
relay_message_id: row.get(9)?,
|
||||||
|
delivery_state: PendingRelayDeliveryState::from_db(row.get(10)?)?,
|
||||||
|
last_error: row.get(11)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_client_offline(destination_user_id: i64, frame_id: u32) -> Result<(), StorageError> {
|
||||||
|
db::with_immediate_transaction(|tx| {
|
||||||
|
tx.execute("UPDATE pending_relays SET delivery_state = 'waiting_client', last_error = 'client_offline' WHERE target_kind = 0 AND destination_id = ?1 AND frame_id = ?2", params![destination_user_id, i64::from(frame_id)])?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause_client_deliveries(destination_user_id: i64) -> Result<(), StorageError> {
|
||||||
|
db::with_immediate_transaction(|tx| {
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE pending_relays SET delivery_state = 'waiting_client', last_error = 'awaiting_state_ack' WHERE target_kind = 0 AND destination_id = ?1 AND delivery_state = 'ready'",
|
||||||
|
[destination_user_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume_client_deliveries(destination_user_id: i64) -> Result<usize, StorageError> {
|
||||||
|
db::with_db(|connection| {
|
||||||
|
Ok(connection.execute("UPDATE pending_relays SET delivery_state = 'ready', last_error = NULL WHERE target_kind = 0 AND destination_id = ?1 AND delivery_state = 'waiting_client'", [destination_user_id])?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume_managed_client_deliveries() -> Result<usize, StorageError> {
|
||||||
|
db::with_db(|connection| {
|
||||||
|
Ok(connection.execute("UPDATE pending_relays SET delivery_state = 'ready', last_error = NULL WHERE target_kind = 0 AND delivery_state = 'waiting_client' AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = pending_relays.destination_id)", [])?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quarantine_target_mismatch(id: i64) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|connection| {
|
||||||
|
connection.execute("UPDATE pending_relays SET delivery_state = 'quarantined', last_error = 'target_recipient_mismatch' WHERE id = ?1", [id])?;
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageError> {
|
pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageError> {
|
||||||
db::with_db(|connection| {
|
db::with_db(|connection| {
|
||||||
|
let target_kind = connection.query_row(
|
||||||
|
"SELECT target_kind FROM pending_relays WHERE id = ?1",
|
||||||
|
[id],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)?;
|
||||||
|
if target_kind == 0 {
|
||||||
|
let destination_id = connection.query_row(
|
||||||
|
"SELECT destination_id FROM pending_relays WHERE id = ?1",
|
||||||
|
[id],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)?;
|
||||||
|
if destination_id != relay.destination_user_id {
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE pending_relays SET delivery_state = 'quarantined', last_error = 'target_recipient_mismatch' WHERE id = ?1",
|
||||||
|
[id],
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
connection.execute("UPDATE pending_relays SET relay_signer_id = ?2, relay_destination_user_id = ?3, relay_message_id = ?4 WHERE id = ?1", params![id, relay.signer_id, relay.destination_user_id, relay.message_id])?;
|
connection.execute("UPDATE pending_relays SET relay_signer_id = ?2, relay_destination_user_id = ?3, relay_message_id = ?4 WHERE id = ?1", params![id, relay.signer_id, relay.destination_user_id, relay.message_id])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,12 @@ impl RouteTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RouteTarget {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::User(id) => write!(formatter, "User({id})"),
|
||||||
|
Self::Iota(id) => write!(formatter, "Iota({id})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use rand_core::RngCore;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
||||||
|
|
@ -39,6 +40,11 @@ use iota_util::route_target::RouteTarget;
|
||||||
const IOTA_KEYRING_PATH: &str = "iota.mk";
|
const IOTA_KEYRING_PATH: &str = "iota.mk";
|
||||||
static IDENTITY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
static IDENTITY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||||
static OMIKRON_TRUST_DIRECTORY: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
static OMIKRON_TRUST_DIRECTORY: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||||
|
static NEXT_CLIENT_EVENT_ID: AtomicU32 = AtomicU32::new(1);
|
||||||
|
|
||||||
|
fn next_client_event_id() -> u32 {
|
||||||
|
NEXT_CLIENT_EVENT_ID.fetch_add(1, Ordering::Relaxed).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
fn record_origin_delivery_failure(signer_id: u64, relay_message_id: &str, failure: &str) {
|
fn record_origin_delivery_failure(signer_id: u64, relay_message_id: &str, failure: &str) {
|
||||||
let Ok(storage_owner) = i64::try_from(signer_id) else {
|
let Ok(storage_owner) = i64::try_from(signer_id) else {
|
||||||
|
|
@ -480,7 +486,7 @@ impl OmikronConnection {
|
||||||
log!("Connecting to Omikron at {}", addr_str);
|
log!("Connecting to Omikron at {}", addr_str);
|
||||||
|
|
||||||
let policy = Policy::default()
|
let policy = Policy::default()
|
||||||
.with_send_mode(SendMode::SingleStreamPerMessage)
|
.with_send_mode(SendMode::PersistentStream)
|
||||||
.with_timeouts(
|
.with_timeouts(
|
||||||
Duration::from_millis(2_000),
|
Duration::from_millis(2_000),
|
||||||
Duration::from_millis(2_000),
|
Duration::from_millis(2_000),
|
||||||
|
|
@ -538,7 +544,6 @@ impl OmikronConnection {
|
||||||
log_t!("omikron_authenticated");
|
log_t!("omikron_authenticated");
|
||||||
|
|
||||||
self.classify_legacy_pending_relays().await;
|
self.classify_legacy_pending_relays().await;
|
||||||
self.flush_pending_relays().await;
|
|
||||||
|
|
||||||
let maintenance_self = self.clone();
|
let maintenance_self = self.clone();
|
||||||
let maintenance_handle = tokio::spawn(async move {
|
let maintenance_handle = tokio::spawn(async move {
|
||||||
|
|
@ -941,6 +946,164 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_event_from_relay(
|
||||||
|
message_type: &str,
|
||||||
|
payload: &DataValue,
|
||||||
|
signer_id: u64,
|
||||||
|
recipient_id: u64,
|
||||||
|
relay_message_id: &str,
|
||||||
|
) -> Option<CommunicationValue> {
|
||||||
|
let sender = DataValue::UnsignedNumber(u128::from(signer_id));
|
||||||
|
let receiver = recipient_id;
|
||||||
|
let event_id = next_client_event_id();
|
||||||
|
|
||||||
|
match message_type {
|
||||||
|
"MessageSend" => {
|
||||||
|
let frame = CommunicationValue::new(CommunicationType::MessageSend)
|
||||||
|
.with_payload(payload.clone());
|
||||||
|
let content = frame.get_data(DataType::Content)?.clone();
|
||||||
|
let send_time = frame.get_data(DataType::SendTime)?.clone();
|
||||||
|
let version = frame.get_data(DataType::VersionNumber)?.clone();
|
||||||
|
let mut message = vec![
|
||||||
|
(DataType::Content, content),
|
||||||
|
(DataType::SendTime, send_time),
|
||||||
|
(DataType::VersionNumber, version),
|
||||||
|
(
|
||||||
|
DataType::RelayMessageId,
|
||||||
|
DataValue::Str(relay_message_id.to_string()),
|
||||||
|
),
|
||||||
|
(DataType::MessageState, DataValue::Str("sent".to_string())),
|
||||||
|
];
|
||||||
|
if let Some(reply_id) = frame.get_data(DataType::ReplyId) {
|
||||||
|
message.push((DataType::ReplyId, reply_id.clone()));
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
CommunicationValue::new(CommunicationType::MessageLive)
|
||||||
|
.with_id(event_id)
|
||||||
|
.with_sender(signer_id)
|
||||||
|
.with_receiver(receiver)
|
||||||
|
.add_typed_default(DataType::SenderId, sender)
|
||||||
|
.add_typed_default(DataType::Message, typed_container(message)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"SetChatSecret" => {
|
||||||
|
let frame = CommunicationValue::new(CommunicationType::SetChatSecret)
|
||||||
|
.with_payload(payload.clone());
|
||||||
|
let chat_id = frame.get_data(DataType::ChatId)?.clone();
|
||||||
|
let secret_id = frame.get_data(DataType::SecretId)?.clone();
|
||||||
|
let version = frame.get_data(DataType::VersionNumber)?.clone();
|
||||||
|
Some(
|
||||||
|
CommunicationValue::new(CommunicationType::ChatSecretForward)
|
||||||
|
.with_id(event_id)
|
||||||
|
.with_sender(signer_id)
|
||||||
|
.with_receiver(receiver)
|
||||||
|
.add_typed_default(DataType::ChatId, chat_id)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::SenderUserId,
|
||||||
|
DataValue::Str(signer_id.to_string()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::RecipientUserId,
|
||||||
|
DataValue::Str(recipient_id.to_string()),
|
||||||
|
)
|
||||||
|
.add_typed_default(DataType::SecretId, secret_id)
|
||||||
|
.add_typed_default(DataType::VersionNumber, version)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Payload,
|
||||||
|
DataValue::Str("available".to_string()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"MessageEdit" => {
|
||||||
|
let frame = CommunicationValue::new(CommunicationType::MessageEdit)
|
||||||
|
.with_payload(payload.clone());
|
||||||
|
Some(
|
||||||
|
CommunicationValue::new(CommunicationType::MessageEditLive)
|
||||||
|
.with_id(event_id)
|
||||||
|
.with_sender(signer_id)
|
||||||
|
.with_receiver(receiver)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Content,
|
||||||
|
frame.get_data(DataType::Content)?.clone(),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::SendTime,
|
||||||
|
frame.get_data(DataType::SendTime)?.clone(),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::VersionNumber,
|
||||||
|
frame.get_data(DataType::VersionNumber)?.clone(),
|
||||||
|
)
|
||||||
|
.add_typed_default(DataType::ChatPartnerId, sender.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"MessageReactionAdd" | "MessageReactionRemove" => {
|
||||||
|
let frame = CommunicationValue::new(CommunicationType::MessageReactionAdd)
|
||||||
|
.with_payload(payload.clone());
|
||||||
|
Some(
|
||||||
|
CommunicationValue::new(CommunicationType::MessageReactionLive)
|
||||||
|
.with_id(event_id)
|
||||||
|
.with_sender(signer_id)
|
||||||
|
.with_receiver(receiver)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Reaction,
|
||||||
|
frame.get_data(DataType::Reaction)?.clone(),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::SendTime,
|
||||||
|
frame.get_data(DataType::SendTime)?.clone(),
|
||||||
|
)
|
||||||
|
.add_typed_default(DataType::ChatPartnerId, sender.clone())
|
||||||
|
.add_typed_default(DataType::SenderId, sender)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Accepted,
|
||||||
|
DataValue::Bool(message_type == "MessageReactionAdd"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"MessageDelete" | "MessageDeleteLive" => {
|
||||||
|
let frame = CommunicationValue::new(CommunicationType::MessageDelete)
|
||||||
|
.with_payload(payload.clone());
|
||||||
|
Some(
|
||||||
|
CommunicationValue::new(CommunicationType::MessageDeleteLive)
|
||||||
|
.with_id(event_id)
|
||||||
|
.with_sender(signer_id)
|
||||||
|
.with_receiver(receiver)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::SendTime,
|
||||||
|
frame.get_data(DataType::SendTime)?.clone(),
|
||||||
|
)
|
||||||
|
.add_typed_default(DataType::ChatPartnerId, sender),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"AddConversation" if recipient_id < signer_id => {
|
||||||
|
let chat_id = format!("{recipient_id}:{signer_id}");
|
||||||
|
Some(
|
||||||
|
CommunicationValue::new(CommunicationType::ChatSecretForward)
|
||||||
|
.with_id(event_id)
|
||||||
|
.with_sender(signer_id)
|
||||||
|
.with_receiver(receiver)
|
||||||
|
.add_typed_default(DataType::ChatId, DataValue::Str(chat_id.clone()))
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::SenderUserId,
|
||||||
|
DataValue::Str(signer_id.to_string()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::RecipientUserId,
|
||||||
|
DataValue::Str(recipient_id.to_string()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::SecretId,
|
||||||
|
DataValue::Str(format!("chat:{chat_id}:main")),
|
||||||
|
)
|
||||||
|
.add_typed_default(DataType::VersionNumber, DataValue::SignedNumber(1))
|
||||||
|
.add_typed_default(DataType::Payload, DataValue::Str("init".to_string())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_relay(self: Arc<Self>, frame: CommunicationValue) {
|
async fn handle_relay(self: Arc<Self>, frame: CommunicationValue) {
|
||||||
let Some(incoming_frame_id) = frame.id() else {
|
let Some(incoming_frame_id) = frame.id() else {
|
||||||
log!("Rejecting Relay without a message id");
|
log!("Rejecting Relay without a message id");
|
||||||
|
|
@ -1046,7 +1209,7 @@ impl OmikronConnection {
|
||||||
|
|
||||||
/* Evaluate recipient policy before reserving relay replay state or
|
/* Evaluate recipient policy before reserving relay replay state or
|
||||||
* persisting the frame, so blocked traffic leaves no durable trace. */
|
* persisting the frame, so blocked traffic leaves no durable trace. */
|
||||||
if recipient_is_local {
|
if recipient_block_policy_applies(recipient_is_local, recipient_id, signer_id) {
|
||||||
match iota_storage::util::blocked_users::is_blocked(recipient_id, signer_id) {
|
match iota_storage::util::blocked_users::is_blocked(recipient_id, signer_id) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
log!(
|
log!(
|
||||||
|
|
@ -1081,11 +1244,7 @@ impl OmikronConnection {
|
||||||
/* An origin Iota is authoritative for receipt disclosure. Inspect
|
/* An origin Iota is authoritative for receipt disclosure. Inspect
|
||||||
* local-origin relay content before reserving or queuing the frame. */
|
* local-origin relay content before reserving or queuing the frame. */
|
||||||
if signer_is_local {
|
if signer_is_local {
|
||||||
let content = match open_verified_relay_content(
|
let content = match open_verified_relay_content(&verified, &[&keyring]) {
|
||||||
&verified,
|
|
||||||
&[&keyring],
|
|
||||||
verified.context.signer_id,
|
|
||||||
) {
|
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log!("Relay origin content verification failed: {}", error);
|
log!("Relay origin content verification failed: {}", error);
|
||||||
|
|
@ -1180,11 +1339,7 @@ impl OmikronConnection {
|
||||||
/* A shared Iota owns both independent replicas before delivering to its
|
/* A shared Iota owns both independent replicas before delivering to its
|
||||||
* local recipient. The destination path below writes the recipient copy. */
|
* local recipient. The destination path below writes the recipient copy. */
|
||||||
if signer_is_local && recipient_is_local && !already_applied {
|
if signer_is_local && recipient_is_local && !already_applied {
|
||||||
let content = match open_verified_relay_content(
|
let content = match open_verified_relay_content(&verified, &[&keyring]) {
|
||||||
&verified,
|
|
||||||
&[&keyring],
|
|
||||||
verified.context.final_recipient_id,
|
|
||||||
) {
|
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log!(
|
log!(
|
||||||
|
|
@ -1241,12 +1396,13 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer_is_local && !recipient_is_local {
|
if signer_is_local && !recipient_is_local {
|
||||||
|
/* Chat-secret versions are immutable. Apply them locally only after
|
||||||
|
* the peer has accepted the same relay, so a peer conflict cannot
|
||||||
|
* leave a newly generated origin version behind. */
|
||||||
|
let defer_chat_secret_commit = frame.is_type(CommunicationType::SetChatSecret);
|
||||||
if !already_applied {
|
if !already_applied {
|
||||||
let content = match open_verified_relay_content(
|
if !defer_chat_secret_commit {
|
||||||
&verified,
|
let content = match open_verified_relay_content(&verified, &[&keyring]) {
|
||||||
&[&keyring],
|
|
||||||
verified.context.signer_id,
|
|
||||||
) {
|
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log!("Relay origin content verification failed: {}", error);
|
log!("Relay origin content verification failed: {}", error);
|
||||||
|
|
@ -1254,7 +1410,10 @@ impl OmikronConnection {
|
||||||
verified.context.signer_id,
|
verified.context.signer_id,
|
||||||
&verified.context.message_id,
|
&verified.context.message_id,
|
||||||
);
|
);
|
||||||
self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData)
|
self.send_relay_response(
|
||||||
|
frame.id(),
|
||||||
|
CommunicationType::ErrorInvalidData,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1276,6 +1435,7 @@ impl OmikronConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let router = match self
|
let router = match self
|
||||||
.hosting_iota_for_user(verified.context.final_recipient_id)
|
.hosting_iota_for_user(verified.context.final_recipient_id)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1384,6 +1544,40 @@ impl OmikronConnection {
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if defer_chat_secret_commit && !already_applied {
|
||||||
|
let content = match open_verified_relay_content(&verified, &[&keyring]) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
log!("Relay origin content verification failed: {}", error);
|
||||||
|
self.send_relay_response(
|
||||||
|
frame.id(),
|
||||||
|
CommunicationType::ErrorInvalidData,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Ok(origin_id) = i64::try_from(verified.context.signer_id) else {
|
||||||
|
self.send_relay_response(
|
||||||
|
frame.id(),
|
||||||
|
CommunicationType::ErrorInvalidData,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Err(error) = message_handlers::apply_verified_relay_content(
|
||||||
|
&verified.context,
|
||||||
|
&content,
|
||||||
|
accepted_at,
|
||||||
|
origin_id,
|
||||||
|
true,
|
||||||
|
) {
|
||||||
|
log!("Relay origin chat-secret application failed: {}", error);
|
||||||
|
self.send_relay_response(frame.id(), CommunicationType::ErrorInternal)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
iota_storage::util::downstream_relay::acknowledge_iota_delivery(
|
iota_storage::util::downstream_relay::acknowledge_iota_delivery(
|
||||||
router,
|
router,
|
||||||
|
|
@ -1417,12 +1611,23 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
log!("Relay origin route returned {}", response.get_type());
|
log!("Relay origin route returned {}", response.get_type());
|
||||||
if response.is_type(CommunicationType::ErrorInternal) {
|
if response.is_type(CommunicationType::ErrorInternal)
|
||||||
|
&& !defer_chat_secret_commit
|
||||||
|
{
|
||||||
record_origin_delivery_failure(
|
record_origin_delivery_failure(
|
||||||
verified.context.signer_id,
|
verified.context.signer_id,
|
||||||
&verified.context.message_id,
|
&verified.context.message_id,
|
||||||
"destination_internal_error",
|
"destination_internal_error",
|
||||||
);
|
);
|
||||||
|
self.send_relay_success(
|
||||||
|
frame.id(),
|
||||||
|
local_iota_id,
|
||||||
|
&verified.context.message_id,
|
||||||
|
accepted_at,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
} else if let Ok(signer_id) = i64::try_from(verified.context.signer_id) {
|
} else if let Ok(signer_id) = i64::try_from(verified.context.signer_id) {
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
iota_storage::util::downstream_relay::reject_iota_delivery(
|
iota_storage::util::downstream_relay::reject_iota_delivery(
|
||||||
|
|
@ -1456,8 +1661,32 @@ impl OmikronConnection {
|
||||||
&verified.context.message_id,
|
&verified.context.message_id,
|
||||||
"destination_unreachable",
|
"destination_unreachable",
|
||||||
);
|
);
|
||||||
|
if defer_chat_secret_commit {
|
||||||
|
if let Ok(signer_id) = i64::try_from(verified.context.signer_id) {
|
||||||
|
if let Err(queue_error) =
|
||||||
|
iota_storage::util::downstream_relay::reject_iota_delivery(
|
||||||
|
router,
|
||||||
|
frame_id,
|
||||||
|
signer_id,
|
||||||
|
&verified.context.message_id,
|
||||||
|
"destination_unreachable",
|
||||||
|
)
|
||||||
|
{
|
||||||
|
log!("Chat-secret relay retry cleanup failed: {}", queue_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.send_relay_response(frame.id(), CommunicationType::ErrorInternal)
|
self.send_relay_response(frame.id(), CommunicationType::ErrorInternal)
|
||||||
.await;
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.send_relay_success(
|
||||||
|
frame.id(),
|
||||||
|
local_iota_id,
|
||||||
|
&verified.context.message_id,
|
||||||
|
accepted_at,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -1515,12 +1744,9 @@ impl OmikronConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut client_event = None;
|
||||||
if !already_applied {
|
if !already_applied {
|
||||||
let content = match open_verified_relay_content(
|
let content = match open_verified_relay_content(&verified, &[&keyring]) {
|
||||||
&verified,
|
|
||||||
&[&keyring],
|
|
||||||
verified.context.final_recipient_id,
|
|
||||||
) {
|
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log!("Relay content verification failed: {}", error);
|
log!("Relay content verification failed: {}", error);
|
||||||
|
|
@ -1570,6 +1796,13 @@ impl OmikronConnection {
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
client_event = Self::client_event_from_relay(
|
||||||
|
&content.message_type,
|
||||||
|
&content.content,
|
||||||
|
verified.context.signer_id,
|
||||||
|
destination,
|
||||||
|
&verified.context.message_id,
|
||||||
|
);
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
relay_replay::mark_applied(verified.context.signer_id, &verified.context.message_id)
|
relay_replay::mark_applied(verified.context.signer_id, &verified.context.message_id)
|
||||||
{
|
{
|
||||||
|
|
@ -1611,8 +1844,10 @@ impl OmikronConnection {
|
||||||
signer_is_local,
|
signer_is_local,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Err(error) = self.send_message(&forwarded).await {
|
if let Some(event) = client_event {
|
||||||
log!("Relay delivery to local client failed: {}", error);
|
if let Err(error) = self.send_message(&event).await {
|
||||||
|
log!("Relay client event delivery failed: {}", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1701,6 +1936,15 @@ impl OmikronConnection {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if matches!(record.target, RouteTarget::User(destination) if i64::try_from(destination).ok() != Some(identity.destination_user_id))
|
||||||
|
{
|
||||||
|
log!(
|
||||||
|
"Quarantining pending Relay {} with a target-recipient mismatch",
|
||||||
|
record.id
|
||||||
|
);
|
||||||
|
let _ = relay_queue::quarantine_target_mismatch(record.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Err(error) = relay_queue::set_relay_identity(record.id, &identity) {
|
if let Err(error) = relay_queue::set_relay_identity(record.id, &identity) {
|
||||||
log!(
|
log!(
|
||||||
"Pending Relay {} ownership backfill failed: {}",
|
"Pending Relay {} ownership backfill failed: {}",
|
||||||
|
|
@ -1713,10 +1957,18 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn flush_pending_relays(&self) {
|
async fn flush_pending_relays(&self) {
|
||||||
let Ok(records) = relay_queue::list(100) else {
|
self.flush_pending_relays_for_user(None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush_pending_relays_for_user(&self, destination_user_id: Option<i64>) {
|
||||||
|
let Ok(records) = relay_queue::list_active(100) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
for record in records {
|
for record in records.into_iter().filter(|record| {
|
||||||
|
destination_user_id.is_none_or(|user_id| {
|
||||||
|
matches!(record.target, RouteTarget::User(destination) if i64::try_from(destination).ok() == Some(user_id))
|
||||||
|
})
|
||||||
|
}) {
|
||||||
if record.relay_signer_id.is_none()
|
if record.relay_signer_id.is_none()
|
||||||
|| record.relay_destination_user_id.is_none()
|
|| record.relay_destination_user_id.is_none()
|
||||||
|| record.relay_message_id.is_none()
|
|| record.relay_message_id.is_none()
|
||||||
|
|
@ -1831,6 +2083,69 @@ impl OmikronConnection {
|
||||||
record.relay_signer_id,
|
record.relay_signer_id,
|
||||||
) && relay_id == Some(message_id)
|
) && relay_id == Some(message_id)
|
||||||
{
|
{
|
||||||
|
if frame.is_type(CommunicationType::SetChatSecret) {
|
||||||
|
let Some(keyring) = self.keyring.read().await.as_ref().cloned()
|
||||||
|
else {
|
||||||
|
log!(
|
||||||
|
"Retaining pending chat-secret Relay {} until the Iota keyring is available",
|
||||||
|
record.id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let resolver_connection = self;
|
||||||
|
let verified = match verify_relay_metadata(
|
||||||
|
&frame,
|
||||||
|
destination_iota,
|
||||||
|
&keyring,
|
||||||
|
move |signer_id| async move {
|
||||||
|
resolver_connection
|
||||||
|
.resolve_relay_signing_keys(signer_id)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
log!(
|
||||||
|
"Retaining pending chat-secret Relay {} after verification failure: {}",
|
||||||
|
record.id,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let content = match open_verified_relay_content(
|
||||||
|
&verified,
|
||||||
|
&[&keyring],
|
||||||
|
) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
log!(
|
||||||
|
"Retaining pending chat-secret Relay {} after content verification failure: {}",
|
||||||
|
record.id,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(error) =
|
||||||
|
message_handlers::apply_verified_relay_content(
|
||||||
|
&verified.context,
|
||||||
|
&content,
|
||||||
|
now_millis_i64(),
|
||||||
|
signer_id,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
log!(
|
||||||
|
"Retaining pending chat-secret Relay {} after origin application failure: {}",
|
||||||
|
record.id,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
iota_storage::util::downstream_relay::acknowledge_iota_delivery(
|
iota_storage::util::downstream_relay::acknowledge_iota_delivery(
|
||||||
destination_iota,
|
destination_iota,
|
||||||
|
|
@ -1917,6 +2232,21 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cv.is_type(CommunicationType::ErrorNoIota)
|
||||||
|
&& cv.get_data(DataType::ErrorType).as_str() == Some("client_offline")
|
||||||
|
&& let (Some(frame_id), Some(destination_id)) = (
|
||||||
|
cv.id(),
|
||||||
|
cv.get_data(DataType::UserId)
|
||||||
|
.as_number()
|
||||||
|
.and_then(|id| i64::try_from(id).ok()),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if let Err(error) = relay_queue::mark_client_offline(destination_id, frame_id) {
|
||||||
|
log!("Pending Relay offline state update failed: {}", error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(msg_id) = cv.id() else {
|
let Some(msg_id) = cv.id() else {
|
||||||
self.handle_message_impl(cv).await;
|
self.handle_message_impl(cv).await;
|
||||||
return;
|
return;
|
||||||
|
|
@ -2271,15 +2601,66 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_client_connected(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn handle_client_connected(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
let _ = self
|
let response = message_handlers::handle_client_connected(cv);
|
||||||
.send_message(&message_handlers::handle_client_connected(cv))
|
let user_id = cv
|
||||||
.await;
|
.require_sender()
|
||||||
|
.ok()
|
||||||
|
.and_then(|id| i64::try_from(id).ok());
|
||||||
|
let session_id = cv
|
||||||
|
.get_data(DataType::SessionId)
|
||||||
|
.as_number()
|
||||||
|
.and_then(|id| i64::try_from(id).ok());
|
||||||
|
|
||||||
|
if response.is_type(CommunicationType::ClientStateSync) {
|
||||||
|
log!(
|
||||||
|
"ClientStateSync user={:?} session={:?} stage=generated",
|
||||||
|
user_id,
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
if let Some(user_id) = user_id
|
||||||
|
&& let Err(error) = relay_queue::pause_client_deliveries(user_id)
|
||||||
|
{
|
||||||
|
log!("Pending Relay state-sync pause failed: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = self.send_message(&response).await {
|
||||||
|
log!(
|
||||||
|
"Initial ClientStateSync delivery failed for user {:?}, session {:?}: {}",
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log!(
|
||||||
|
"ClientStateSync user={:?} session={:?} stage=sent_to_omikron",
|
||||||
|
user_id,
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = self.send_message(&response).await {
|
||||||
|
log!("ClientConnected response delivery failed: {}", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_client_state_ack(self: Arc<Self>, cv: &CommunicationValue) {
|
async fn handle_client_state_ack(self: Arc<Self>, cv: &CommunicationValue) {
|
||||||
let _ = self
|
let response = message_handlers::handle_client_state_ack(cv);
|
||||||
.send_message(&message_handlers::handle_client_state_ack(cv))
|
let user_id = cv
|
||||||
.await;
|
.require_sender()
|
||||||
|
.ok()
|
||||||
|
.and_then(|id| i64::try_from(id).ok());
|
||||||
|
if self.send_message(&response).await.is_ok()
|
||||||
|
&& response.is_type(CommunicationType::Success)
|
||||||
|
&& let Some(user_id) = user_id
|
||||||
|
{
|
||||||
|
if let Err(error) = relay_queue::resume_client_deliveries(user_id) {
|
||||||
|
log!("Pending Relay client resume failed: {}", error);
|
||||||
|
} else {
|
||||||
|
self.flush_pending_relays_for_user(Some(user_id)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mutation_live_message(
|
fn mutation_live_message(
|
||||||
|
|
@ -3075,6 +3456,14 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recipient_block_policy_applies(
|
||||||
|
recipient_is_local: bool,
|
||||||
|
recipient_id: i64,
|
||||||
|
signer_id: i64,
|
||||||
|
) -> bool {
|
||||||
|
recipient_is_local && recipient_id != signer_id
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Global Instance
|
// Global Instance
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -3287,4 +3676,11 @@ mod tests {
|
||||||
OmikronError::Rejected(CommunicationType::ErrorNotFound, _)
|
OmikronError::Rejected(CommunicationType::ErrorNotFound, _)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipient_block_policy_skips_self_delivery() {
|
||||||
|
assert!(!recipient_block_policy_applies(true, 42, 42));
|
||||||
|
assert!(recipient_block_policy_applies(true, 42, 43));
|
||||||
|
assert!(!recipient_block_policy_applies(false, 42, 43));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue