openmls - MLS Protocol for Dart
Dart bindings for OpenMLS, providing a Rust implementation of the Messaging Layer Security (MLS) protocol (RFC 9420) for secure group messaging.
Platform Support
| Android | iOS | macOS | Linux | Windows | Web | |
|---|---|---|---|---|---|---|
| Support | SDK 24+ | 13.0+ | 10.15+ | arm64, x64 | x64 | WASM |
| Arch | arm64, armv7, x64 | arm64, x64 (sim) | arm64, x64 | arm64, x64 | x64 | wasm32 |
Features
- MLS Protocol (RFC 9420): Secure group messaging with forward secrecy and post-compromise security
- Group Key Agreement: Efficient tree-based group key agreement (TreeKEM)
- Post-Quantum (Experimental): Ten hybrid and pure post-quantum ciphersuites (X-Wing, ML-KEM-768/1024, ML-DSA-44/65/87) — see Post-Quantum Support
- Encrypted Storage: All MLS state encrypted at rest — SQLCipher on native, Web Crypto AES-256-GCM on WASM
- Basic & X.509 Credentials: Support for both credential types
- Flutter & CLI Support: Works with Flutter apps and standalone Dart CLI applications
- Automatic Builds: Native libraries downloaded automatically via build hooks
- High Performance: Direct Rust integration via Flutter Rust Bridge
Post-Quantum Support (Experimental)
supportedCiphersuites() returns thirteen ciphersuites. Three are the
IANA-registered MLS 1.0 suites from RFC 9420; the other ten are experimental
post-quantum suites on provisional code points. All thirteen are exercised
end-to-end by the test suite — a full group lifecycle each, not merely a key
generation.
The classical suites (interoperable)
| Suite | Value |
|---|---|
mls128DhkemX25519Aes128GcmSha256Ed25519 |
0x0001 |
mls128DhkemP256Aes128GcmSha256P256 |
0x0002 |
mls128DhkemX25519Chacha20Poly1305Sha256Ed25519 |
0x0003 |
The post-quantum suites (experimental)
Hybrid means the KEM keeps a classical component, so group secrets stay
confidential if either half holds. PQ-only means there is no classical
fallback: a break of ML-KEM (or ML-DSA) breaks the suite outright.
| Suite | Value | KEM | Signature |
|---|---|---|---|
mls256XwingChacha20Poly1305Sha256Ed25519 |
0x004D | Hybrid (X-Wing) | Ed25519 |
mls192Mlkem1024Aes256GcmSha384P384 |
0x0042 | PQ-only ML-KEM-1024 | ECDSA P-384 |
mls128Mlkem768X25519Aes256GcmSha384Ed25519 |
0x004E | Hybrid (ML-KEM-768 + X25519) | Ed25519 |
mls128Mlkem768X25519Aes128GcmSha256Ed25519 |
0x004F | Hybrid (ML-KEM-768 + X25519) | Ed25519 |
mls128Mlkem768Aes256GcmSha384P256 |
0x0050 | PQ-only ML-KEM-768 | ECDSA P-256 |
mls192Mlkem768Aes256GcmSha384Mldsa65 |
0x0051 | PQ-only ML-KEM-768 | ML-DSA-65 |
mls128Mlkem768X25519Chacha20Poly1305Sha384Mldsa44 |
0x0052 | Hybrid (ML-KEM-768 + X25519) | ML-DSA-44 |
mls256Mlkem1024Aes256GcmSha512Mldsa87 |
0x0906 | PQ-only ML-KEM-1024 | ML-DSA-87 |
mls256Mlkem1024Aes256GcmSha384Mldsa87 |
0x0907 | PQ-only ML-KEM-1024 | ML-DSA-87 |
mls128Mlkem768Aes256GcmSha384Ed25519 |
0xF042 | PQ-only ML-KEM-768 | Ed25519 |
Read before using — honest limitations:
- Experimental, not standardized. None of these code points is registered
with IANA. The nine
MLKEM/MLDSAsuites carry the provisional values from draft-ietf-mls-pq-ciphersuites, which may be renumbered or withdrawn before publication; X-Wing (0x004D) comes from an expired individual draft. When official suites are published, groups created on these will need to migrate. We will track the official suites and provide a migration path. - Limited interoperability. In practice only OpenMLS-based stacks (and, for X-Wing, ts-mls) implement these. Use them in closed deployments where every client uses this library or OpenMLS — not for cross-vendor federation.
PQ-onlysuites have no classical fallback. If you want the harvest-now-decrypt-later protection without betting solely on lattice assumptions, choose aHybridrow.- The underlying implementations are pre-1.0. ML-KEM, ML-DSA and X-Wing are provided by the RustCrypto and libcrux stacks at pre-1.0 versions.
X-Wing specifically
mls256XwingChacha20Poly1305Sha256Ed25519 uses the X-Wing hybrid KEM
(draft-connolly-cfrg-xwing-kem):
ML-KEM-768 combined with X25519, so group secrets stay confidential if either
component remains unbroken. It is the only suite whose HPKE operations run on
libcrux (formally verified ML-KEM); every
other suite, classical and post-quantum alike, runs on RustCrypto.
- libcrux is pre-1.0 and not fully audited. Its ML-KEM source is formally verified (hax/F*: correctness, secret independence, panic freedom), but compiled binaries carry no side-channel-resistance verification, and the maintainers themselves advise consultation before production use.
- The X-Wing construction itself is peer-reviewed (IND-CCA secure if either ML-KEM-768 or X25519 holds) and its wire format has been stable across draft revisions.
What your peers see
A leaf node advertises the ciphersuites you are willing to accept. Unless you
pass explicit capabilities, OpenMLS fills that list with all thirteen
suites above — so peers may choose an experimental one for a group you join.
To advertise a narrower set, pass MlsCapabilities with an explicit
ciphersuites list (as raw u16 values) to createGroupWithBuilder or
proposeSelfUpdate. createKeyPackage itself takes no capabilities argument,
so a key package built with it advertises the full list — use
createKeyPackageWithOptions and set KeyPackageOptions.capabilities to narrow
that one too.
Implementation Status
| Category | Status | Description |
|---|---|---|
| Group Lifecycle | Done | Create, join (Welcome, external commit), leave, inspect |
| Member Management | Done | Add, remove, swap members |
| Messaging | Done | Encrypt/decrypt application messages with AAD |
| Proposals | Done | Add, remove, self-update, PSK, custom, group context extensions |
| Commits | Done | Pending proposals, flexible commit, merge/clear |
| Key Packages | Done | Create with options (lifetime, last-resort), read and check the validity window |
| Credentials | Done | Basic and X.509 credential types |
| State Queries | Done | Members, epoch, extensions, ratchet tree, group info, PSK export |
| Storage | Done | Encrypted at rest via MlsEngine (SQLCipher / Web Crypto) |
Full API reference
Key Packages: createKeyPackage, createKeyPackageWithOptions, keyPackageLifetime, checkLifetimeAt
Group Lifecycle: createGroup, createGroupWithBuilder, joinGroupFromWelcome, joinGroupFromWelcomeWithOptions, inspectWelcome, exportWelcomeSecret, joinGroupExternalCommit, joinGroupExternalCommitV2
State Queries: groupId, groupEpoch, groupIsActive, groupMembers, groupCiphersuite, groupOwnIndex, groupCredential, groupExtensions, groupPendingProposals, groupHasPendingProposals, groupMemberAt, groupMemberLeafIndex, groupOwnLeafNode, groupConfirmationTag, groupConfiguration, groupEpochAuthenticator, exportRatchetTree, exportGroupInfo, exportSecret, exportGroupContext, getPastResumptionPsk
Mutations: addMembers, addMembersWithoutUpdate, removeMembers, selfUpdate, selfUpdateWithNewSigner, swapMembers, leaveGroup, leaveGroupViaSelfRemove
Proposals: proposeAdd, proposeRemove, proposeSelfUpdate, proposeSelfUpdateWithNewSigner, proposeExternalPsk, proposeGroupContextExtensions, proposeCustomProposal, proposeRemoveMemberByCredential, removePendingProposal
Commit/Merge: commitToPendingProposals, mergePendingCommit, clearPendingCommit, clearPendingProposals, setConfiguration, updateGroupContextExtensions, flexibleCommit
Messages: createMessage, processMessage, processMessageWithInspect, mlsMessageExtractGroupId, mlsMessageExtractEpoch, mlsMessageContentType
Engine & Storage: close, isClosed, schemaVersion, deleteGroup, deleteKeyPackage
Installation
Add to your pubspec.yaml:
dependencies:
openmls: ^x.x.x
Native libraries are downloaded automatically during build via Dart build hooks.
No Rust required for end users - precompiled binaries are downloaded from GitHub Releases.
Usage
import 'dart:convert';
import 'dart:typed_data';
import 'package:openmls/openmls.dart';
void main() async {
// Initialize the library
await Openmls.init();
// Create an MlsEngine with encrypted storage.
// - Native: SQLCipher database at the given file path
// - Web: IndexedDB with AES-256-GCM encryption via Web Crypto API
// Use ":memory:" for ephemeral in-memory storage (testing).
final encryptionKey = Uint8List(32); // 32-byte key — store in platform secure storage!
final engine = await MlsEngine.create(
dbPath: ':memory:',
encryptionKey: encryptionKey,
);
// Generate signing key pair
final ciphersuite = MlsCiphersuite.mls128DhkemX25519Aes128GcmSha256Ed25519;
final keyPair = MlsSignatureKeyPair.generate(ciphersuite: ciphersuite);
final signerBytes = serializeSigner(
ciphersuite: ciphersuite,
privateKey: keyPair.privateKey(),
publicKey: keyPair.publicKey(),
);
// Create a group
final config = MlsGroupConfig.defaultConfig(ciphersuite: ciphersuite);
final group = await engine.createGroup(
config: config,
signerBytes: signerBytes,
credentialIdentity: utf8.encode('alice'),
signerPublicKey: keyPair.publicKey(),
);
print('Created group: ${group.groupId}');
// Close engine (releases DB connection and encryption key resources)
await engine.close();
// Clean up FRB runtime (optional, for CLI apps exiting)
Openmls.cleanup();
}
Storage
All MLS state is stored in a Rust-owned encrypted database via MlsEngine:
| Platform | Backend | Encryption |
|---|---|---|
| Native (iOS, Android, macOS, Linux, Windows) | SQLCipher | AES-256 full-database encryption |
| Web (WASM) | IndexedDB | AES-256-GCM per-value encryption via crypto.subtle |
// Create engine with a 32-byte encryption key.
// Store the key in platform secure storage (Keychain, Android Keystore, etc.)
final engine = await MlsEngine.create(
dbPath: 'mls_data.db', // file path on native, IDB name on web
encryptionKey: myKey, // 32-byte AES-256 key
);
// All operations go through the engine
final group = await engine.createGroup(...);
await engine.addMembers(...);
// Close the engine to release the DB connection and encryption key resources.
// After close, all operations fail with "MlsEngine is closed".
// Useful for screen lock / app background scenarios.
await engine.close();
// Re-create from secure storage on unlock
final engine2 = await MlsEngine.create(dbPath: 'mls_data.db', encryptionKey: myKey);
On WASM, the encryption key is imported as a non-extractable CryptoKey via the Web Crypto API. Raw key bytes are zeroized from WASM memory immediately after import.
Known Limitations
Web: flutter build web --wasm (dart2wasm) is not supported
This package works with the standard flutter build web (dart2js) target. It does not currently work when the host app is compiled with flutter build web --wasm / flutter run -d chrome --wasm (dart2wasm). Calls to the Rust side fail with:
Type 'JSValue' is not a subtype of type 'List<dynamic>' in type cast
This is an upstream limitation in flutter_rust_bridge — its generated Dart decoders rely on implicit JS-array casts that work on dart2js but fail under dart2wasm. The pattern is hardcoded in FRB's codegen templates, so it affects every FRB-based Dart package, not just this one. Tracking upstream: flutter_rust_bridge#2575.
| Command | Status |
|---|---|
flutter run -d chrome |
Works (dart2js) |
flutter build web |
Works (dart2js) |
flutter run -d chrome --wasm |
Not supported (dart2wasm) |
flutter build web --wasm |
Not supported (dart2wasm) |
The Rust core of openmls ships as a .wasm module in both modes — --wasm only changes what the Dart code compiles to. Crypto performance and functionality are equivalent.
Web: flutter run -d chrome can skip the build hook and leave web/pkg/ empty
The build hook provisions the WASM module into your app's web/pkg/ directory.
flutter build web always reaches it. flutter run -d chrome reaches it only while
Flutter's build system considers its dart_build target out of date — and that target's
cache key does not include the target platform. A debug flutter run keys its build
directory on the engine revision, the entrypoint, the build mode and the output path
alone, so a debug run for another platform (flutter run -d macos, say) leaves behind a
dart_build stamp naming its own dependencies; the next flutter run -d chrome finds
every one of them unchanged, logs Skipping target: dart_build, and never invokes the
hook. With web/pkg/ not already provisioned, RustLib.init() then fails on a 404 for
pkg/openmls_frb.js.
The hook cannot defend against this — the skip happens above hooks_runner, so nothing
the hook declares as a dependency is ever read. Any one of these unblocks it, and
flutter run -d chrome serves web/pkg/ normally afterwards:
flutter build web # provisions web/pkg/ through the same hook
rm -f build/*/dart_build.stamp # drop the stale stamp, then run again
flutter clean # the blunt version of the same thing
Building from Source
For End Users
No setup required! Precompiled native libraries are downloaded automatically from GitHub Releases during flutter build.
For Contributors / Source Builds
- Flutter (>=3.38.0)
- Rust (1.91+) — must match
rust-versioninrust/Cargo.toml - FVM (recommended for version management)
- Make (for build commands; see the Windows note in CONTRIBUTING.md)
Setup
# Clone the repository
git clone https://github.com/djx-y-z/openmls_dart.git
cd openmls_dart
# Install FVM and dependencies
make setup
# Generate Dart bindings
make codegen
# Build native library
make build
# Run tests
make test
# See all available commands
make help
Developing Rust API
- Add your Rust functions in
rust/src/api/:
// rust/src/api/greeting.rs
pub fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
- Register the module in
rust/src/api/mod.rs:
pub mod greeting;
- Generate Dart bindings:
make codegen
- Build and test:
make build
make test
Building Native Libraries
Native libraries are pre-built and downloaded automatically via build hooks. If you need to build them locally:
# Build for current platform
make build
# Build with specific target
make build ARGS="--target aarch64-apple-darwin"
# Build for Android
make build-android
# Build for Web (WASM)
make build-web
CI / Version Management
# Check for new openmls versions
make check-new-openmls-version
# Check for new copier template versions
make check-template-updates
# Check deployment target consistency (iOS/macOS/Android)
make check-targets
# Update Cargo.lock dependencies
make rust-update
# Generate AI-powered changelog entry (needs AI_MODELS + a provider key)
make update-changelog ARGS="--version v1.0.0"
The CI automatically checks for new openmls releases daily and creates PRs with:
- Updated
pubspec.yamland version badges - Updated
Cargo.lock(if successful) - Regenerated FRB bindings (if successful)
- AI-generated CHANGELOG entry, when
AI_MODELSnames a model with a key (see CONTRIBUTING); otherwise the pull request is labelledchangelog-needed
It also checks for copier template updates daily. When one is found it applies
it with copier update and opens a pull request carrying the result — a draft
when copier could not merge something, or when the update failed to record the
new version in .copier-answers.yml.
Architecture
┌─────────────────────────────────────────────────┐
│ OpenMLS (Rust crate) │ Core MLS implementation
├─────────────────────────────────────────────────┤
│ MlsEngine + EncryptedDb (Rust) │ Encrypted storage layer
├─────────────────────────────────────────────────┤
│ rust/src/api/*.rs (Rust wrappers) │ FRB-annotated functions
├─────────────────────────────────────────────────┤
│ lib/src/rust/*.dart (FRB generated) │ Auto-generated Dart API
├─────────────────────────────────────────────────┤
│ Your Dart application code │ Uses MlsEngine
└─────────────────────────────────────────────────┘
Security Notes
Key Properties:
- MLS Protocol (RFC 9420) - Standardized group key agreement with forward secrecy and post-compromise security
- Rust Implementation - All cryptographic operations run in Rust (OpenMLS with RustCrypto backend; the experimental X-Wing post-quantum KEM is delegated to libcrux)
- Encrypted at Rest - All MLS state encrypted via SQLCipher (native) or Web Crypto AES-256-GCM (WASM)
- Web Crypto on WASM - Encryption key stored as non-extractable
CryptoKeyviacrypto.subtle— raw bytes never persist in WASM memory - Memory Safety - Rust's ownership model prevents memory-related vulnerabilities
- No
unsafecode in the wrapper layer (exceptSend + SyncforCryptoKeyon single-threaded WASM)
Best Practices:
- Keep the library updated to the latest version
- Store the 32-byte encryption key in platform secure storage (Keychain, Android Keystore,
flutter_secure_storage) - Never log or expose serialized key material (
serializeSigner(),privateKey()) - Use
SecureBytes.wrap()or.zeroize()for sensitive data (serialized keys, shared secrets) — see SECURITY.md - Process MLS messages in order to maintain group state consistency
- Web deployment: Enable strict CSP headers (
script-src 'self') and serve over HTTPS
See SECURITY.md for full security guidelines.
Acknowledgements
This library would not be possible without OpenMLS, which provides the underlying Rust implementation of the MLS protocol.
Contributing
Contributions are welcome! Please read our Contributing Guidelines before submitting issues or pull requests.
For major changes, please open an issue first to discuss what you would like to change.
Security
See SECURITY.md for security policy and reporting vulnerabilities.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Third-party notices
The prebuilt native library is statically linked against its Rust dependency
tree (MIT, Apache-2.0, BSD, ISC and similar). Those licenses require their
notices to travel with any binary distribution — including an application that
embeds the library — and Flutter's LicenseRegistry does not cover them,
because it aggregates LICENSE files of pub packages and Rust crates are not
pub packages.
THIRD_PARTY_NOTICES.txt ships at the root of this
package and inside every native release archive. It is generated from the
resolved dependency graph across all released targets — build edges included,
because that is how vendored native code reaches the binary: a *-src crate
carrying C sources is a build-dependency of its *-sys wrapper — and CI
verifies it stays in sync with Cargo.lock. Where a crate ships no licence
file of its own, the canonical text of the licence it declares is supplied in
its place, so the file delivers the licences and not just their names.
Regenerate it with make third-party-notices after a dependency change;
make rust-update already does that for you.
The file is deliberately not declared under flutter: assets: — a
package-declared asset is bundled into every consuming application whether or
not it is used, and most applications never display these notices. To surface
them at runtime, copy the file into your own assets and register it:
# your app's pubspec.yaml
flutter:
assets:
- assets/THIRD_PARTY_NOTICES.txt
LicenseRegistry.addLicense(() async* {
final text = await rootBundle.loadString('assets/THIRD_PARTY_NOTICES.txt');
yield LicenseEntryWithLineBreaks(const ['openmls'], text);
});
Related Projects
- OpenMLS - The underlying Rust MLS library
- RFC 9420 - The Messaging Layer Security (MLS) Protocol
- Flutter Rust Bridge - Dart/Flutter <-> Rust binding generator
Libraries
- openmls
- Dart wrapper for OpenMLS — a Rust implementation of the Messaging Layer Security (MLS) protocol (RFC 9420)