Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d289a45
Added unreleased title to changelog file
Adel2411 Mar 28, 2026
cffdbe7
rust(core/error): Add `KeyRotationFailed` variant
nferhat Mar 30, 2026
0ab1668
rust(evfs/helpers): Implement `decrypt_segment_raw`
nferhat Mar 30, 2026
4e6d0af
feat: Add `vault_rotate_keys`
nferhat Apr 1, 2026
5f3101f
fix(evfs): Fix `vault_rotate_key` locking
nferhat Apr 1, 2026
b9a63b5
evfs: Add `vault_rotate_key` tests
nferhat Apr 1, 2026
3a9acbb
evfs: Check for previous `.rotating` vault on `vault_open`
nferhat Apr 1, 2026
2620f79
fix(evfs): Harden vault_rotate_key
Adel-Ayoub Apr 2, 2026
03b2458
Merge pull request #114 from MicroClub-USTHB/ferhat/atomic-reencryption
Adel-Ayoub Apr 2, 2026
bbf0a0d
feat(evfs): add ExportFailed error variant and .mvex archive format t…
Adel-Ayoub Apr 2, 2026
6c642ec
feat(evfs): implement vault_export() for .mvex encrypted archives
Adel-Ayoub Apr 2, 2026
2455bc3
feat(evfs): add vault_export and archive format tests
Adel-Ayoub Apr 2, 2026
3a6ea28
fix(evfs): harden vault_export key zeroization, error cleanup, and bo…
Adel-Ayoub Apr 2, 2026
48976df
Merge pull request #115 from MicroClub-USTHB/adelayoub/evfs-vault-export
Adel-Ayoub Apr 2, 2026
237de9f
feat(core): add ImportFailed error variant for evfs archive import
Adel2411 Apr 4, 2026
4c8ead2
feat(evfs): implement vault_import with zeroization and crash recovery
Adel2411 Apr 4, 2026
2966733
test(evfs): add vault_import test suite ensuring full byte matching a…
Adel2411 Apr 4, 2026
de3aeff
fix(evfs): harden vault_import - integer overflow, zeroization, cleanup
Adel-Ayoub Apr 4, 2026
458eba3
Merge pull request #116 from MicroClub-USTHB/112-evfs-import-vault
Adel-Ayoub Apr 4, 2026
8fd85e5
feat(evfs): add Dart wrappers for rotateKey, export, importVault + in…
Adel-Ayoub Apr 4, 2026
447c013
Merge pull request #117 from MicroClub-USTHB/adelayoub/evfs-dart-key-…
Adel-Ayoub Apr 4, 2026
ddd66d6
feat(example): add key rotation, export, import to vault demo UI + fi…
Adel-Ayoub Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

### Changed

### Fixed

## [v0.3.3](https://github.com/MicroClub-USTHB/M-Security/releases/tag/v0.3.3) - 2026-03-28

### Added
Expand Down
169 changes: 145 additions & 24 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:m_security/m_security.dart' as msec;
import 'package:m_security/m_security.dart';
import 'package:m_security/src/rust/api/encryption.dart' as rust_enc;
import 'package:m_security/src/rust/api/hashing.dart' as hashing;
import 'package:m_security/src/rust/api/compression.dart';
import 'package:m_security/src/rust/api/evfs/types.dart' as rust_evfs_types;

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -113,7 +110,7 @@ class _HashingTabState extends State<_HashingTab> {
onPressed: _loading
? null
: () => _run('BLAKE3', () async {
final h = await hashing.blake3Hash(
final h = await msec.blake3Hash(
data: utf8.encode(_input.text),
);
return _hex(h);
Expand All @@ -124,7 +121,7 @@ class _HashingTabState extends State<_HashingTab> {
onPressed: _loading
? null
: () => _run('SHA-3', () async {
final h = await hashing.sha3Hash(
final h = await msec.sha3Hash(
data: utf8.encode(_input.text),
);
return _hex(h);
Expand Down Expand Up @@ -186,7 +183,7 @@ class _EncryptionTabState extends State<_EncryptionTab> {
final _input = TextEditingController(text: 'Secret message');
String _algo = 'AES-256-GCM';
Uint8List? _encrypted;
rust_enc.CipherHandle? _cipher;
msec.CipherHandle? _cipher;
String _encHex = '';
String _decrypted = '';
bool _loading = false;
Expand All @@ -199,12 +196,12 @@ class _EncryptionTabState extends State<_EncryptionTab> {
});
try {
final key = _algo == 'AES-256-GCM'
? await rust_enc.generateAes256GcmKey()
: await rust_enc.generateChacha20Poly1305Key();
? await msec.generateAes256GcmKey()
: await msec.generateChacha20Poly1305Key();
_cipher = _algo == 'AES-256-GCM'
? await rust_enc.createAes256Gcm(key: key)
: await rust_enc.createChacha20Poly1305(key: key);
_encrypted = await rust_enc.encrypt(
? await msec.createAes256Gcm(key: key)
: await msec.createChacha20Poly1305(key: key);
_encrypted = await msec.encrypt(
cipher: _cipher!,
plaintext: Uint8List.fromList(utf8.encode(_input.text)),
aad: Uint8List(0),
Expand All @@ -220,7 +217,7 @@ class _EncryptionTabState extends State<_EncryptionTab> {
if (_encrypted == null || _cipher == null) return;
setState(() => _loading = true);
try {
final plain = await rust_enc.decrypt(
final plain = await msec.decrypt(
cipher: _cipher!,
ciphertext: _encrypted!,
aad: Uint8List(0),
Expand Down Expand Up @@ -419,11 +416,11 @@ class _StreamingTabState extends State<_StreamingTab> {

// Create cipher
final key = _algo == 'AES-256-GCM'
? await rust_enc.generateAes256GcmKey()
: await rust_enc.generateChacha20Poly1305Key();
? await msec.generateAes256GcmKey()
: await msec.generateChacha20Poly1305Key();
final cipher = _algo == 'AES-256-GCM'
? await rust_enc.createAes256Gcm(key: key)
: await rust_enc.createChacha20Poly1305(key: key);
? await msec.createAes256Gcm(key: key)
: await msec.createChacha20Poly1305(key: key);

// Encrypt
setState(() => _status = 'Encrypting ${_sizeKb.text}KB...');
Expand Down Expand Up @@ -453,8 +450,8 @@ class _StreamingTabState extends State<_StreamingTab> {

// Decrypt
final cipher2 = _algo == 'AES-256-GCM'
? await rust_enc.createAes256Gcm(key: key)
: await rust_enc.createChacha20Poly1305(key: key);
? await msec.createAes256Gcm(key: key)
: await msec.createChacha20Poly1305(key: key);

if (_compAlgo == 'None') {
await StreamingService.decryptFile(
Expand Down Expand Up @@ -492,7 +489,7 @@ class _StreamingTabState extends State<_StreamingTab> {
Future<void> _testStreamHash() async {
setState(() {
_loading = true;
_status = 'Creating test file for hashing...';
_status = 'Creating test file for msec...';
_progress = 0;
});
try {
Expand All @@ -507,11 +504,11 @@ class _StreamingTabState extends State<_StreamingTab> {
await File(filePath).writeAsBytes(data);

// One-shot hash for comparison
final oneshotHash = await hashing.blake3Hash(data: data);
final oneshotHash = await msec.blake3Hash(data: data);

// Streaming hash
setState(() => _status = 'Streaming BLAKE3 hash...');
final hasher = await hashing.createBlake3();
final hasher = await msec.createBlake3();
final streamHash = await StreamingService.hashFile(
filePath: filePath,
hasher: hasher,
Expand Down Expand Up @@ -621,8 +618,10 @@ class _VaultTabState extends State<_VaultTab> {
bool _vaultOpen = false;
String _compAlgo = 'Zstd';
final _resizeMb = TextEditingController(text: '10');
String _keyMgmtInfo = '';
String? _exportPath;

rust_evfs_types.VaultHandle? _handle;
msec.VaultHandle? _handle;
Uint8List? _key;
String? _vaultPath;

Expand All @@ -631,7 +630,7 @@ class _VaultTabState extends State<_VaultTab> {
try {
final dir = await Directory.systemTemp.createTemp('demo_vault');
_vaultPath = '${dir.path}/demo.vault';
_key = await rust_enc.generateAes256GcmKey();
_key = await msec.generateAes256GcmKey();
final sizeMb = int.tryParse(_vaultSizeMb.text) ?? 5;
_handle = await VaultService.create(
path: _vaultPath!,
Expand Down Expand Up @@ -893,6 +892,89 @@ class _VaultTabState extends State<_VaultTab> {
setState(() => _loading = false);
}

Future<void> _rotateKey() async {
if (!_vaultOpen || _handle == null) return;
setState(() => _loading = true);
try {
final newKey = await msec.generateAes256GcmKey();
_handle = await VaultService.rotateKey(handle: _handle!, newKey: newKey);
_key = newKey;
_keyMgmtInfo = 'Key rotated — all segments re-encrypted under new key';
_status = 'Key rotation complete';
await _refreshList();
await _refreshCapacity();
} catch (e) {
_keyMgmtInfo = 'Rotation error: $e';
}
setState(() => _loading = false);
}

Future<void> _exportVault() async {
if (!_vaultOpen || _handle == null) return;
setState(() => _loading = true);
try {
final dir = Directory(_vaultPath!).parent;
_exportPath = '${dir.path}/export.mvex';
final wrappingKey = await msec.generateAes256GcmKey();
await VaultService.export(
handle: _handle!,
wrappingKey: wrappingKey,
exportPath: _exportPath!,
);
final size = await File(_exportPath!).length();
_keyMgmtInfo =
'Exported to ${_exportPath!.split('/').last} '
'(${_fmtBytes(BigInt.from(size))})\n'
'Wrapping key: ${_hex(wrappingKey).substring(0, 16)}...';
_exportWrappingKey = wrappingKey;
_status = 'Vault exported';
} catch (e) {
_keyMgmtInfo = 'Export error: $e';
}
setState(() => _loading = false);
}

Uint8List? _exportWrappingKey;

Future<void> _importVault() async {
if (_exportPath == null || _exportWrappingKey == null) {
setState(() => _keyMgmtInfo = 'Export a vault first');
return;
}
setState(() => _loading = true);
try {
if (_vaultOpen && _handle != null) {
await VaultService.close(handle: _handle!);
}

final dir = Directory(_vaultPath!).parent;
final importPath = '${dir.path}/imported.vault';
final importKey = await msec.generateAes256GcmKey();

_handle = await VaultService.importVault(
archivePath: _exportPath!,
wrappingKey: _exportWrappingKey!,
destPath: importPath,
newMasterKey: importKey,
algorithm: 'aes-256-gcm',
capacityBytes: (int.tryParse(_vaultSizeMb.text) ?? 5) * 1024 * 1024,
);
_key = importKey;
_vaultPath = importPath;
_vaultOpen = true;

await _refreshList();
await _refreshCapacity();
_keyMgmtInfo =
'Imported ${_segments.length} segments into new vault\n'
'Path: ${importPath.split('/').last}';
_status = 'Vault imported';
} catch (e) {
_keyMgmtInfo = 'Import error: $e';
}
setState(() => _loading = false);
}

@override
Widget build(BuildContext context) {
return ListView(
Expand Down Expand Up @@ -1098,6 +1180,45 @@ class _VaultTabState extends State<_VaultTab> {

const Divider(height: 24),

// Key Management
Text(
'Key Management',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: FilledButton.tonalIcon(
onPressed: _loading ? null : _rotateKey,
icon: const Icon(Icons.autorenew, size: 18),
label: const Text('Rotate Key'),
),
),
const SizedBox(width: 6),
Expanded(
child: FilledButton.tonalIcon(
onPressed: _loading ? null : _exportVault,
icon: const Icon(Icons.upload_file, size: 18),
label: const Text('Export'),
),
),
const SizedBox(width: 6),
Expanded(
child: FilledButton.tonalIcon(
onPressed: _loading || _exportPath == null
? null
: _importVault,
icon: const Icon(Icons.download, size: 18),
label: const Text('Import'),
),
),
],
),
if (_keyMgmtInfo.isNotEmpty) _ResultCard('Key Mgmt', _keyMgmtInfo),

const Divider(height: 24),

// Streaming segment I/O
Text('Streaming I/O', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Expand Down
Loading
Loading