Symptom: Messages fail to decrypt with MessageDecryptionError::AeadError or UnableToDecrypt.Common Causes:
Messages processed out of order
Problem: Application messages must be processed after the commit that created their epoch.Solution:
// Sort messages by epoch before processinglet mut messages = get_pending_messages();messages.sort_by_key(|m| { match m { ProtocolMessage::PublicMessage(pm) => pm.epoch(), ProtocolMessage::PrivateMessage(pm) => pm.epoch(), }});for message in messages { group.process_message(&provider, message)?;}
Missed commits
Problem: Group state is behind because commits were not processed.Solution:
// Request all messages since last known epochlet current_epoch = group.epoch();let missing = fetch_messages_since_epoch(current_epoch);// Process all commits in orderfor message in missing { match group.process_message(&provider, message) { Ok(ProcessedMessage::StagedCommitMessage(staged)) => { group.merge_staged_commit(&provider, *staged)?; }, Err(e) => eprintln!("Error: {:?}", e), _ => {} }}
Implement a message ordering and queuing system in your application to prevent missed commits.
Secret tree not persisted correctly
Problem: Secret tree state was not saved after processing messages.Solution:
Ensure your storage provider properly persists the secret tree:
impl StorageProvider for MyStorage { fn write_secret_tree<V: Entity>( &self, key: &[u8], value: &V, ) -> Result<(), Self::Error> { // Must actually persist to storage self.save(key, value)?; // Don't just store in memory cache Ok(()) }}
Version 0.7.1+ fixes a bug where secret trees weren’t persisted correctly. Update to the latest version.
Attempting to decrypt own messages
Problem: Trying to decrypt messages you sent.Solution:
match group.process_message(&provider, message) { Err(ProcessMessageError::ValidationError( ValidationError::CannotDecryptOwnMessage )) => { // This is your own message - skip it continue; }, Ok(msg) => { // Process message from others }, Err(e) => return Err(e),}
Message too old (SecretTreeError::TooDistantInThePast)
Problem: Message is from too many generations in the past.Solution:
match group.process_message(&provider, message) { Err(ProcessMessageError::ValidationError( ValidationError::UnableToDecrypt( MessageDecryptionError::SecretTree( SecretTreeError::TooDistantInThePast ) ) )) => { // Message is too old - request resend request_message_resend(message.sender())?; }, _ => {}}
Prevention: Process messages promptly and maintain synchronized state.
Symptom:ValidationError::WrongEpoch when processing messages.Cause: Message is for a different epoch than the group’s current state.Solutions:
match group.process_message(&provider, message) { Err(ProcessMessageError::ValidationError( ValidationError::WrongEpoch )) => { let msg_epoch = get_message_epoch(&message); let group_epoch = group.epoch(); if msg_epoch > group_epoch { // Message from future epoch - need to process intervening commits let missing = fetch_commits(group_epoch, msg_epoch); for commit in missing { process_commit(&mut group, commit)?; } // Retry original message group.process_message(&provider, message)?; } else { // Message from past epoch - already processed // Ignore or request fresh message } }, _ => {}}
Symptom: Error when creating a group: group with same ID already exists.Cause: OpenMLS 0.8.0+ prevents accidentally overwriting existing groups.Solutions:
// Option 1: Delete old group firstprovider.storage().delete_group(&group_id)?;let group = MlsGroup::new(/* ... */)?;// Option 2: Use replace_old_group flaglet group = MlsGroupBuilder::new() .with_group_id(group_id) .replace_old_group(true) // Explicitly replace .build(&provider, &signer, credential_with_key)?;// Option 3: Generate unique group IDlet group_id = GroupId::random(&provider.rand())?;let group = MlsGroup::new( &provider, &signer, &mls_group_config, credential_with_key,)?;
Symptom:ProcessMessageError::StorageError when processing commits.Cause: Storage provider failures are now properly reported (0.7.0+).Solution:
impl StorageProvider for MyStorage { fn read<V: Entity>(&self, key: &[u8]) -> Result<Option<V>, Self::Error> { // Properly handle all error cases match self.inner_read(key) { Ok(value) => Ok(Some(value)), Err(NotFoundError) => Ok(None), // Not found is not an error Err(e) => Err(e), // Other errors should propagate } }}
Symptom: Different group members have diverged states.Solution: Use fork resolution (requires fork-resolution feature):
#[cfg(feature = "fork-resolution")]{ use openmls::group::fork_resolution::*; // Detect fork let fork_detector = ForkDetector::new(&group, &provider)?; if fork_detector.has_fork() { // Resolve fork by syncing with other members let resolution = fork_detector.resolve( &provider, &other_member_states, )?; // Apply resolution group.apply_fork_resolution(&provider, resolution)?; }}
Symptom: Cannot validate messages from past epochs.Cause: Credentials not properly stored for historical validation.Solution:
// Ensure credentials are stored when processing commitsif let ProcessedMessage::StagedCommitMessage(staged) = processed { // Store credentials for new members for add_proposal in staged.add_proposals() { let credential = add_proposal.key_package().credential(); store_credential(credential)?; } group.merge_staged_commit(&provider, *staged)?;}
Symptom: Errors when compiling to WebAssembly.Solution:
# Cargo.toml[dependencies]openmls = { version = "0.8", features = ["js"] }openmls_rust_crypto = { version = "0.3", features = ["js"] }# Or for libcrux provideropenmls_libcrux_crypto = { version = "0.3", features = ["js"] }# Update getrandom for WASMgetrandom = { version = "0.3.4", features = ["js"] }
// Initialize provider for WASM#[cfg(target_arch = "wasm32")]let provider = OpenMlsRustCrypto::default();
// 1. Clear old proposals regularlygroup.clear_pending_proposals();// 2. Don't keep multiple epochs in memory// Process and merge commits immediatelyif let ProcessedMessage::StagedCommitMessage(staged) = msg { group.merge_staged_commit(&provider, *staged)?; // Staged commit is dropped, freeing memory}// 3. Use storage-backed provider instead of memory// Don't use MemoryStorage for production
#[test]fn test_message_processing() { // Load test fixture let test_message = include_bytes!("fixtures/valid_commit.bin"); // Process with test group let result = group.process_message(&provider, test_message); // Verify expected behavior assert!(result.is_ok());}