Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/openmls/openmls/llms.txt

Use this file to discover all available pages before exploring further.

OpenMLS continuously persists group state to a configured storage provider. This allows groups to be loaded later and ensures critical key material is properly managed for forward secrecy.

Storage provider overview

All group operations interact with a StorageProvider that handles:
  • Group state persistence
  • Key material storage and deletion
  • Proposal store management
  • Epoch secrets and resumption PSKs
use openmls::prelude::*;
use openmls_rust_crypto::OpenMlsRustCrypto;

// Create a provider with storage
let provider = OpenMlsRustCrypto::default();

// Create a group - automatically stored
let alice_group = MlsGroup::new(
    &provider,
    &alice_signature_keys,
    &mls_group_create_config,
    alice_credential_with_key,
)?;

// Group state is now persisted

Loading groups

Groups can be loaded from storage using their group ID:
use openmls::prelude::*;

let group_id = alice_group.group_id().clone();

// Later, in a different session...
let loaded_group = MlsGroup::load(
    provider.storage(),
    &group_id,
)?;

if let Some(group) = loaded_group {
    println!("Group loaded successfully");
    println!("Current epoch: {}", group.epoch());
} else {
    println!("Group not found in storage");
}
For loading to work, the group must have been stored previously through normal group operations or an explicit call to store().

Automatic persistence

OpenMLS automatically writes group state after most operations:
use openmls::prelude::*;

// Creates and stores the group
let mut alice_group = MlsGroup::new(
    &provider,
    &alice_signature_keys,
    &config,
    credential,
)?;

// Automatically updates storage
alice_group.add_members(
    &provider,
    &alice_signature_keys,
    &[bob_key_package],
)?;

// Automatically updates storage
alice_group.self_update(
    &provider,
    &alice_signature_keys,
    LeafNodeParameters::default(),
)?;

// No manual save needed - all operations persist automatically

Explicit storage operations

You can also explicitly store and delete groups:
use openmls::prelude::*;

// Explicitly store a group
alice_group.store(provider.storage())?;

// Delete a group from storage
alice_group.delete(provider.storage())?;
Deleting a group removes all associated state including key material. This operation is irreversible and should be used carefully.

Forward secrecy considerations

OpenMLS implements forward secrecy by deleting old key material:
use openmls::prelude::*;

// When processing an encrypted message:
let processed = alice_group.process_message(&provider, message)?;

// Old decryption keys are immediately deleted from storage
// Even if the storage backend is compromised later,
// old messages cannot be decrypted

Storage provider requirements

Critical for forward secrecy: Storage providers MUST ensure that values deleted through any delete_* functions are irrevocably deleted with no copies kept.This includes:
  • delete_encryption_key_pair()
  • delete_epoch_keypairs()
  • delete_own_leaf_nodes()
  • Other delete operations
Keeping copies or soft-deleting would compromise forward secrecy.

What gets stored

The storage provider persists multiple types of data:

Group state

// Core group state
- Group ID
- Current epoch
- Group context
- Ratchet tree
- Proposal store
- Configuration

Key material

// Encryption keys
- Current epoch keypairs
- HPKE private keys
- Signature key pairs

// Secret state
- Message secrets
- Epoch secrets
- Resumption PSKs

Proposals and messages

// Pending state
- Queued proposals
- Own leaf nodes (for pending updates)

Storage backends

OpenMLS supports different storage backends:

In-memory storage

use openmls_rust_crypto::OpenMlsRustCrypto;

// Default: in-memory storage (lost when process exits)
let provider = OpenMlsRustCrypto::default();
In-memory storage loses all data when the process exits. Use persistent storage for production applications.

Persistent storage

Implement the StorageProvider trait for your database:
use openmls::storage::StorageProvider;

struct MyDatabaseStorage {
    // Your database connection
}

impl StorageProvider for MyDatabaseStorage {
    type Error = MyError;
    
    fn write_group_state(
        &self,
        group_id: &GroupId,
        group_state: &MlsGroupState,
    ) -> Result<(), Self::Error> {
        // Store in your database
    }
    
    fn group_state(
        &self,
        group_id: &GroupId,
    ) -> Result<Option<MlsGroupState>, Self::Error> {
        // Load from your database
    }
    
    fn delete_group_state(
        &self,
        group_id: &GroupId,
    ) -> Result<(), Self::Error> {
        // Irrevocably delete from database
    }
    
    // Implement other required methods...
}

Key package management

Key packages are also managed through storage:
use openmls::prelude::*;

// Key packages are automatically stored
let key_package = KeyPackage::builder()
    .build(ciphersuite, &provider, signer, credential)?;

// Key package bundle is in storage
// When used in a Welcome, it's automatically deleted (unless last resort)
Last resort key packages are NOT deleted from storage when used. This allows them to be reused for multiple welcomes.

Past epoch secrets

When max_past_epochs is configured, old secrets are retained:
use openmls::prelude::*;

let config = MlsGroupCreateConfig::builder()
    .max_past_epochs(3)  // Keep secrets for 3 past epochs
    .build();

let alice_group = MlsGroup::new(&provider, signer, &config, credential)?;

// Message secrets for the last 3 epochs are kept in storage
// Older secrets are deleted for forward secrecy

Resumption PSKs

Resumption PSKs are stored per epoch:
use openmls::prelude::*;

let config = MlsGroupCreateConfig::builder()
    .number_of_resumption_psks(5)
    .build();

let alice_group = MlsGroup::new(&provider, signer, &config, credential)?;

// After each epoch transition, resumption PSK is stored
// Only the last 5 are kept in storage

Migration and backup

When migrating to a new device or backing up:
use openmls::prelude::*;

// Export group state
let group_id = alice_group.group_id().clone();
let exported_state = alice_group.export_secret(
    provider.crypto(),
    "backup",
    b"backup-context",
    32,
)?;

// On new device, you'll need to:
// 1. Transfer all storage data securely
// 2. Import signature keys
// 3. Load the group

let restored_group = MlsGroup::load(provider.storage(), &group_id)?;
Group state includes sensitive key material. Ensure backups are encrypted and stored securely. Prefer server-side storage over client backups when possible.

Storage best practices

1
Use persistent storage
2
Implement a proper database backend:
3
// Good: Persistent database
let provider = MyDatabaseProvider::new(db_connection);

// Bad: In-memory (for production)
let provider = OpenMlsRustCrypto::default();
4
Implement atomic operations
5
Ensure storage operations are atomic:
6
impl StorageProvider for MyStorage {
    fn write_group_state(
        &self,
        group_id: &GroupId,
        state: &MlsGroupState,
    ) -> Result<(), Self::Error> {
        // Use database transaction
        let tx = self.db.transaction()?;
        tx.write(group_id, state)?;
        tx.commit()?;  // Atomic commit
        Ok(())
    }
}
7
Ensure secure deletion
8
Properly delete key material:
9
impl StorageProvider for MyStorage {
    fn delete_encryption_key_pair(
        &self,
        public_key: &[u8],
    ) -> Result<(), Self::Error> {
        // Not sufficient:
        // self.db.mark_deleted(public_key)?;
        
        // Correct: Irrevocable deletion
        self.db.permanently_delete(public_key)?;
        self.db.purge_deleted_records()?;
        Ok(())
    }
}
10
Handle concurrent access
11
Protect against race conditions:
12
use std::sync::Mutex;

struct MyStorage {
    db: Mutex<Database>,
}

impl StorageProvider for MyStorage {
    fn write_group_state(
        &self,
        group_id: &GroupId,
        state: &MlsGroupState,
    ) -> Result<(), Self::Error> {
        let db = self.db.lock().unwrap();
        db.write(group_id, state)?;
        Ok(())
    }
}

Storage errors

Handle storage errors appropriately:
use openmls::prelude::*;

let result = MlsGroup::load(provider.storage(), &group_id);

match result {
    Ok(Some(group)) => {
        // Group loaded successfully
    }
    Ok(None) => {
        // Group not found in storage
        println!("Group does not exist");
    }
    Err(e) => {
        // Storage backend error
        eprintln!("Storage error: {:?}", e);
    }
}
  • StorageProvider - Trait for implementing storage backends
  • MlsGroup::load() - Load group from storage
  • MlsGroup::store() - Explicitly store group
  • MlsGroup::delete() - Delete group from storage

Next steps

Group configuration

Configure past epoch and PSK storage

Key updates

Understand key rotation and forward secrecy