
A native Rust security SDK for Flutter, providing high-performance cryptographic services, streaming encryption with compression, an encrypted virtual file system (EVFS), and secure memory management. All operations run in Rust through Flutter Rust Bridge. No Dart-level crypto, no platform channels.
Built and maintained by the Dev Department of MicroClub, the computer science club at USTHB (University of Science and Technology Houari Boumediene, Algiers).
| Category | Algorithm / Feature | Highlights |
|---|---|---|
| AEAD Encryption | AES-256-GCM | Industry-standard, hardware-accelerated on most CPUs |
| ChaCha20-Poly1305 | Optimized for mobile (no AES hardware needed) | |
| Streaming Encryption | AES-256-GCM / ChaCha20 | Chunk-based processing with progress callbacks |
| Compression | Zstd, Brotli | Configurable levels, integrated into streaming and EVFS |
| Hashing | BLAKE3 | Ultra-fast, one-shot and streaming |
| SHA-3-256 (Keccak) | NIST-standard, one-shot and streaming | |
| Password Hashing | Argon2id | PHC winner, Mobile and Desktop presets |
| Key Derivation | HKDF-SHA256 | RFC 5869, extract-then-expand with domain separation |
| Encrypted VFS (EVFS) | .vault container |
Named segments, WAL recovery, shadow index, secure deletion |
| Segment Enhancements | Metadata, rename, parallel | Per-segment key-value tags, rename without re-encryption, concurrent reads |
| Key Management | Rotation, export/import | Atomic re-encryption, .mvex portable archives |
| Zero-Copy I/O | mmap + DCO codec | Memory-mapped vault reads, zero-copy Rust-to-Dart transfers |
Security by design:
ZeroizeOnDrop)OsRng)panic = "abort" in release profile, preventing undefined behavior from panics crossing FFIclippy::unwrap_used = "deny", ensuring all operations return Result<T, CryptoError>mlock() pins mmap'd ciphertext pages to prevent swap-to-disk (unix)Add to your pubspec.yaml:
dependencies:
m_security: ^0.3.5
Then run:
flutter pub get
M-Security compiles Rust code during the Flutter build. You need:
bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
| Platform | Requirements |
|---|---|
| Android | Android NDK (r27c recommended) |
| iOS / macOS | Xcode with command line tools |
| Linux | clang, cmake, ninja-build, pkg-config, libgtk-3-dev |
| Windows | Visual Studio Build Tools + LLVM |
Rust compilation is handled automatically by Cargokit during flutter build / flutter run.
Initialize the Rust library once at app startup:
import 'package:m_security/m_security.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
runApp(const MyApp());
}
All examples below use a single import:
import 'package:m_security/m_security.dart';
final aes = AesGcmService();
await aes.initWithRandomKey();
// Encrypt and decrypt raw bytes
final encrypted = await aes.encrypt(plaintext);
final decrypted = await aes.decrypt(encrypted);
// Convenience: encrypt and decrypt UTF-8 strings
final ciphertext = await aes.encryptString('sensitive data');
final original = await aes.decryptString(ciphertext);
final chacha = Chacha20Service();
await chacha.initWithRandomKey();
// Basic encrypt and decrypt
final encrypted = await chacha.encryptString('sensitive data');
final original = await chacha.decryptString(encrypted);
// With Associated Authenticated Data (AAD)
final ct = await chacha.encryptString('payload', aad: 'metadata');
final pt = await chacha.decryptString(ct, aad: 'metadata');
Both ciphers output nonce || ciphertext || tag. Nonces (12 bytes) are auto-generated and authentication tags (16 bytes) are appended automatically.
// Hash a password (returns PHC-format string)
final hash = await argon2IdHash(password: 'hunter2');
// Verify a password against a hash
await argon2IdVerify(phcHash: hash, password: 'hunter2');
// Explicit preset selection
final hash = await argon2IdHash(
password: 'hunter2',
preset: Argon2Preset.desktop, // 256 MiB, t=4, p=8
);
The default preset is selected at compile time: Argon2Preset.mobile (64 MiB, t=3, p=4) unless built with -DIS_DESKTOP=true.
// Derive a key from input key material
final key = MHKDF.derive(
ikm: masterKeyBytes,
salt: saltBytes, // optional
info: Uint8List.fromList('encryption-key'.codeUnits),
outputLen: 32,
);
// Domain separation: same master key, different derived keys
final encKey = MHKDF.derive(ikm: master, info: utf8.encode('enc'), outputLen: 32);
final macKey = MHKDF.derive(ikm: master, info: utf8.encode('mac'), outputLen: 32);
// Two-phase: extract PRK, then expand
final prk = MHKDF.extract(ikm: masterKeyBytes, salt: saltBytes);
final derived = await MHKDF.expand(prk: prk, info: infoBytes, outputLen: 32);
Output length must be between 1 and 8160 bytes (RFC 5869 limit for SHA-256: 255 * 32).
import 'package:m_security/src/rust/api/streaming.dart';
// Encrypt a file in chunks with progress
final encrypted = await streamEncrypt(
plaintext: largeData,
algorithm: StreamAlgorithm.aes256Gcm,
compression: CompressionAlgorithm.zstd,
compressionLevel: 3,
onProgress: (progress) => print('${(progress * 100).toInt()}%'),
);
// Decrypt
final decrypted = await streamDecrypt(
ciphertext: encrypted,
algorithm: StreamAlgorithm.aes256Gcm,
compression: CompressionAlgorithm.zstd,
);
import 'package:m_security/m_security.dart';
// Create a 10 MB vault with AES-256-GCM
final handle = await VaultService.create(
path: '/path/to/my.vault',
key: key,
algorithm: 'aes-256-gcm',
capacityBytes: 10 * 1024 * 1024,
);
// Write a segment (with optional compression and metadata)
await VaultService.write(
handle: handle,
name: 'secret.txt',
data: utf8.encode('confidential'),
compression: CompressionConfig(algorithm: CompressionAlgorithm.zstd),
metadata: {'mime': 'text/plain', 'author': 'alice'},
);
// Read it back (decompression is automatic, metadata included)
final result = await VaultService.read(handle: handle, name: 'secret.txt');
print(result.data); // decrypted bytes
print(result.metadata); // {'mime': 'text/plain', 'author': 'alice'}
// List segments, delete, close
final segments = await VaultService.list(handle: handle);
await VaultService.delete(handle: handle, name: 'secret.txt');
await VaultService.close(handle: handle);
// Rotate master key (re-encrypts all segments atomically)
final newHandle = await VaultService.rotateKey(handle: handle, newKey: newKey);
// Old handle is invalidated; use newHandle from here
// Export vault to portable encrypted archive
await VaultService.export(
handle: handle,
wrappingKey: wrappingKey,
exportPath: '/path/to/backup.mvex',
);
// Import vault from archive (creates new vault with fresh key)
final imported = await VaultService.importVault(
archivePath: '/path/to/backup.mvex',
wrappingKey: wrappingKey,
destPath: '/path/to/restored.vault',
newMasterKey: localKey,
algorithm: 'aes-256-gcm',
capacityBytes: 10 * 1024 * 1024,
);
// Rename a segment (index-only, no re-encryption)
await VaultService.renameSegment(handle: handle, oldName: 'draft.txt', newName: 'final.txt');
// Explicit flush — persist in-memory index to disk
await VaultService.flush(handle: handle);
// Parallel read — decrypt multiple segments concurrently
final results = await VaultService.readParallel(
handle: handle,
names: ['file1.bin', 'file2.bin', 'file3.bin'],
);
for (final r in results) {
if (r.error != null) print('${r.name}: failed — ${r.error}');
else print('${r.name}: ${r.data.length} bytes');
}
// Health check (read-only, no I/O)
final health = await VaultService.health(handle: handle);
print('Consistent: ${health.isConsistent}');
print('Fragmentation: ${(health.fragmentationRatio * 100).toStringAsFixed(1)}%');
// Defragment — compact segments, coalesce free space (WAL-protected)
final result = await VaultService.defragment(handle: handle);
print('Moved ${result.segmentsMoved} segments, reclaimed ${result.bytesReclaimed} bytes');
// Resize vault capacity (grow or shrink)
await VaultService.resize(handle: handle, newCapacityBytes: 20 * 1024 * 1024);
For one-shot and streaming hashing, use the lower-level FFI API directly:
import 'package:m_security/src/rust/api/hashing.dart';
// One-shot hashing (32-byte output)
final blake3Digest = await blake3Hash(data: inputBytes);
final sha3Digest = await sha3Hash(data: inputBytes);
// Streaming: process data in chunks
final hasher = createBlake3(); // or createSha3()
await hasherUpdate(handle: hasher, data: chunk1);
await hasherUpdate(handle: hasher, data: chunk2);
final digest = await hasherFinalize(handle: hasher);
// Reset and reuse
await hasherReset(handle: hasher);
Key design decisions:
CipherHandle and HasherHandle are #[frb(opaque)]. Dart holds a pointer, never raw key bytes.Box<dyn Encryption> and Box<dyn Hasher> with Send + Sync + 'static enable runtime algorithm selection.SecretBuffer which derives ZeroizeOnDrop. Memory is zeroed when handles are dropped.panic = "abort" in release profile. All FFI functions return Result<T, CryptoError>.MSEC magic header with version and algorithm identifiers for forward compatibility.CipherHandle)create_aes256_gcm(key: Vec<u8>) -> Result<CipherHandle>
create_chacha20_poly1305(key: Vec<u8>) -> Result<CipherHandle>
encrypt(cipher, plaintext, aad) -> Result<Vec<u8>>
decrypt(cipher, ciphertext, aad) -> Result<Vec<u8>>
generate_aes256_gcm_key() -> Result<Vec<u8>>
generate_chacha20_poly1305_key() -> Result<Vec<u8>>
encryption_algorithm_id(cipher) -> String
HasherHandle)blake3_hash(data) -> Vec<u8> (one-shot, 32 bytes)
sha3_hash(data) -> Vec<u8> (one-shot, 32 bytes)
create_blake3() -> HasherHandle (streaming)
create_sha3() -> HasherHandle (streaming)
hasher_update(handle, data) -> Result<()>
hasher_reset(handle) -> Result<()>
hasher_finalize(handle) -> Result<Vec<u8>>
hasher_algorithm_id(handle) -> Result<String>
argon2id_hash(password, preset) -> Result<String> (PHC)
argon2id_hash_with_salt(password, salt, preset) -> Result<String> (PHC)
argon2id_verify(phc_hash, password) -> Result<()>
Presets: Mobile (64 MiB, t=3, p=4) | Desktop (256 MiB, t=4, p=8)
hkdf_derive(ikm, salt?, info, output_len) -> Result<Vec<u8>> (one-shot)
hkdf_extract(ikm, salt?) -> Result<Vec<u8>> (PRK)
hkdf_expand(prk, info, output_len) -> Result<Vec<u8>>
| Platform | Target | Status |
|---|---|---|
| Android | `aarch64-linux-and |
$ claude mcp add M-Security \
-- python -m otcore.mcp_server <graph>