core_crypto/mls/conversation/
duplicate.rsuse crate::prelude::MlsConversation;
use crate::{CryptoError, MlsError};
use mls_crypto_provider::MlsCryptoProvider;
use openmls::prelude::{ContentType, FramedContentBodyIn, Proposal, PublicMessageIn, Sender};
impl MlsConversation {
pub(crate) fn is_duplicate_message(
&self,
backend: &MlsCryptoProvider,
msg: &PublicMessageIn,
) -> Result<bool, CryptoError> {
let (sender, content_type) = (msg.sender(), msg.body().content_type());
match (content_type, sender) {
(ContentType::Commit, Sender::Member(_) | Sender::NewMemberCommit) => {
if let Some(msg_ct) = msg.confirmation_tag() {
let group_ct = self.group.compute_confirmation_tag(backend).map_err(MlsError::from)?;
Ok(msg_ct == &group_ct)
} else {
Err(CryptoError::InternalMlsError)
}
}
(ContentType::Proposal, Sender::Member(_) | Sender::NewMemberProposal) => {
match msg.body() {
FramedContentBodyIn::Proposal(proposal) => {
let proposal = Proposal::from(proposal.clone()); let already_exists = self.group.pending_proposals().any(|pp| pp.proposal() == &proposal);
Ok(already_exists)
}
_ => Err(CryptoError::InternalMlsError),
}
}
(_, _) => Ok(false),
}
}
}
#[cfg(test)]
mod tests {
use crate::{test_utils::*, CryptoError};
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
async fn decrypting_duplicate_member_commit_should_fail(case: TestCase) {
if !case.is_pure_ciphertext() {
run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
let unknown_commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
alice_central.context.clear_pending_commit(&id).await.unwrap();
let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
alice_central.context.commit_accepted(&id).await.unwrap();
bob_central
.context
.decrypt_message(&id, &commit.to_bytes().unwrap())
.await
.unwrap();
let decrypt_duplicate = bob_central
.context
.decrypt_message(&id, &commit.to_bytes().unwrap())
.await;
assert!(matches!(decrypt_duplicate.unwrap_err(), CryptoError::DuplicateMessage));
let decrypt_lost_commit = bob_central
.context
.decrypt_message(&id, &unknown_commit.to_bytes().unwrap())
.await;
assert!(matches!(decrypt_lost_commit.unwrap_err(), CryptoError::StaleCommit));
})
})
.await
}
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
async fn decrypting_duplicate_external_commit_should_fail(case: TestCase) {
run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
let gi = alice_central.get_group_info(&id).await;
let unknown_ext_commit = bob_central
.context
.join_by_external_commit(gi.clone(), case.custom_cfg(), case.credential_type)
.await
.unwrap()
.commit;
bob_central
.context
.clear_pending_group_from_external_commit(&id)
.await
.unwrap();
let ext_commit = bob_central
.context
.join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
.await
.unwrap()
.commit;
bob_central
.context
.merge_pending_group_from_external_commit(&id)
.await
.unwrap();
alice_central
.context
.decrypt_message(&id, &ext_commit.to_bytes().unwrap())
.await
.unwrap();
let decryption = alice_central
.context
.decrypt_message(&id, &ext_commit.to_bytes().unwrap())
.await;
assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
let decryption = alice_central
.context
.decrypt_message(&id, &unknown_ext_commit.to_bytes().unwrap())
.await;
assert!(matches!(decryption.unwrap_err(), CryptoError::StaleCommit));
})
})
.await
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
async fn decrypting_duplicate_proposal_should_fail(case: TestCase) {
run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
bob_central
.context
.decrypt_message(&id, &proposal.to_bytes().unwrap())
.await
.unwrap();
let decryption = bob_central
.context
.decrypt_message(&id, &proposal.to_bytes().unwrap())
.await;
assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
bob_central.context.commit_pending_proposals(&id).await.unwrap();
bob_central.context.commit_accepted(&id).await.unwrap();
let decryption = bob_central
.context
.decrypt_message(&id, &proposal.to_bytes().unwrap())
.await;
assert!(matches!(decryption.unwrap_err(), CryptoError::StaleProposal));
})
})
.await
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
async fn decrypting_duplicate_external_proposal_should_fail(case: TestCase) {
run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
let epoch = alice_central.context.conversation_epoch(&id).await.unwrap();
let ext_proposal = bob_central
.context
.new_external_add_proposal(id.clone(), epoch.into(), case.ciphersuite(), case.credential_type)
.await
.unwrap();
alice_central
.context
.decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
.await
.unwrap();
let decryption = alice_central
.context
.decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
.await;
assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
alice_central.context.commit_pending_proposals(&id).await.unwrap();
alice_central.context.commit_accepted(&id).await.unwrap();
let decryption = alice_central
.context
.decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
.await;
assert!(matches!(decryption.unwrap_err(), CryptoError::StaleProposal));
})
})
.await
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
async fn decrypting_duplicate_application_message_should_fail(case: TestCase) {
run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
let msg = b"Hello bob";
let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
bob_central.context.decrypt_message(&id, &encrypted).await.unwrap();
let decryption = bob_central.context.decrypt_message(&id, &encrypted).await;
assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
})
})
.await
}
}