Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f7b503a
feat(streaming): add low-level streaming primitives …
Adel-Ayoub Feb 26, 2026
d7582e8
Merge pull request #43 from MicroClub-USTHB/adelayoub/streaming-core
Adel-Ayoub Feb 26, 2026
7ffe868
build(frb): untrack generated Dart bindings
Adel-Ayoub Feb 26, 2026
b2b39be
feat(streaming): add high-level streaming encrypt/decrypt API
Adel-Ayoub Feb 26, 2026
bcf6fd3
fix(streaming): resolve clippy lints
Adel-Ayoub Feb 26, 2026
e41b970
Merge pull request #45 from MicroClub-USTHB/adelayoub/streaming-encry…
Adel-Ayoub Feb 26, 2026
0cb3318
fix(frb): hide NoopEncryption from codegen to fix broken Dart import …
Adel-Ayoub Feb 26, 2026
370c78a
feat(streaming): add streaming file hash with BLAKE3/SHA-3 support
Adel-Ayoub Feb 26, 2026
4f8c5af
fix(frb): hide NoopEncryption from codegen to fix broken Dart import …
Adel-Ayoub Feb 26, 2026
5e52a47
Merge pull request #46 from MicroClub-USTHB/adelayoub/streaming-hash
Adel-Ayoub Feb 26, 2026
0f90aaa
fix(streaming): split hash_file into feed+finalize for FRB compatibil…
Adel-Ayoub Feb 26, 2026
2a03e8f
feat: Add streaming service wrapper and integration tests
aasmaa01 Feb 27, 2026
f9a7064
fix(streaming): wrap FRB streams to catch out-of-band errors FRB uses…
Adel-Ayoub Feb 27, 2026
060d275
Merge pull request #47 from MicroClub-USTHB/asma/streaming-dart
Adel-Ayoub Feb 27, 2026
c35d7ff
Merge pull request #48 from MicroClub-USTHB/streaming
Adel-Ayoub Feb 27, 2026
e98a7f9
feat(compression): add Zstd/Brotli core with algorithm enum and MIME-…
Adel-Ayoub Mar 2, 2026
61f3c6a
Merge pull request #58 from MicroClub-USTHB/adelayoub/compression-core
Adel-Ayoub Mar 2, 2026
dc654ad
Added new compression byte to StreamHeader
Adel2411 Mar 2, 2026
5ebdf74
Fixed StreamHeader caller in api/streaming file and tested it
Adel2411 Mar 2, 2026
69ddcc5
Added compression from/to u8 helpers
Adel2411 Mar 2, 2026
2939123
Created new encrypt/decrypt & compress functions
Adel2411 Mar 2, 2026
1f975ad
Added FRB entry points for new compress encrypt/decrypt functions
Adel2411 Mar 2, 2026
46e2b4b
Wrote all required tests for new compression encrypt/decrypt functions
Adel2411 Mar 2, 2026
8f56347
Fixed broken test issue
Adel2411 Mar 2, 2026
76497aa
Switch helper functions to CompressionAlgorithm methods
Adel2411 Mar 2, 2026
25c2be9
Switched compression attribute from u8 to CompressionAlgorithm, kept …
Adel2411 Mar 2, 2026
60f7e63
Fixed padding striping issue to only strip last chunk now
Adel2411 Mar 2, 2026
bd81eb5
Merge pull request #59 from MicroClub-USTHB/Adel/streaming-compression
Adel-Ayoub Mar 2, 2026
8a5e6e0
feat(compression): stream-compress then chunk for actual space savings
Adel-Ayoub Mar 2, 2026
531d3cf
refactor(streaming): split streaming.rs into directory module
Adel-Ayoub Mar 2, 2026
cecb903
fix(streaming): eliminate double-decrypt timing leak with read-ahead …
Adel-Ayoub Mar 2, 2026
f59ca82
fix(compression): use DecompressorWriter for streaming brotli decompr…
Adel-Ayoub Mar 2, 2026
9da8009
Merge remote-tracking branch 'origin/compression' into nayla/dart-wra…
Naaylla Mar 3, 2026
e01c68f
Created Dart Wrapper for compression pipeline
Naaylla Mar 3, 2026
e3b07d1
refactor(compression): move internals t core/ and enable FRB codegen
Adel-Ayoub Mar 3, 2026
1a18a7f
feat(dart): add CompressionService wrapper for compress+encrypt pipeline
Adel-Ayoub Mar 3, 2026
4a57180
test(compression): add 5 integration tests and fix path extension par…
Adel-Ayoub Mar 3, 2026
e923eb9
Merge pull request #60 from MicroClub-USTHB/nayla/dart-wrapper-compre…
Adel-Ayoub Mar 3, 2026
96fccd4
build(compression): make compression a default feature for FRB compat
Adel-Ayoub Mar 3, 2026
420e2b0
fix(streaming): add fsync before rename in decrypt paths
Adel-Ayoub Mar 3, 2026
1018702
fix(compression): cap decompressor output to prevent decompression bombs
Adel-Ayoub Mar 3, 2026
3607b16
fix(compression): replace unwrap with expect in streaming tests
Adel-Ayoub Mar 3, 2026
c852a7f
Merge pull request #61 from MicroClub-USTHB/compression
Adel-Ayoub Mar 3, 2026
94998c2
feat(evfs): add vault error variants to CryptoError
Adel-Ayoub Mar 5, 2026
453fdf3
feat(evfs): add evfs module scaffolding
Adel-Ayoub Mar 5, 2026
7dd312e
feat(evfs): add vault format structures and segment index
Adel-Ayoub Mar 5, 2026
1321abc
feat(evfs): update vault format structures and segment index
Adel-Ayoub Mar 5, 2026
008d968
Merge pull request #62 from MicroClub-USTHB/adelayoub/evfs-format
Adel-Ayoub Mar 5, 2026
4dc5161
build(evfs): add subtle crate for constant-time comparison
Adel-Ayoub Mar 5, 2026
3714cba
feat(evfs): add per-segment encryption, checksums, and secure deletion
Adel-Ayoub Mar 5, 2026
ab2d378
feat(evfs): add per-segment encryption, checksums, and secure deletion
Adel-Ayoub Mar 5, 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
280 changes: 280 additions & 0 deletions example/integration_test/streaming_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:m_security/src/rust/api/encryption.dart';
import 'package:m_security/src/rust/frb_generated.dart';
import 'package:m_security/src/streaming/streaming_service.dart';
import 'package:m_security/src/rust/api/hashing.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async => await RustLib.init());

group('Streaming', () {
test('encrypt then decrypt file roundtrip', () async {
//Create temp file with known content
//→ encrypt → decrypt → compare bytes identical
final tempDir = await Directory.systemTemp.createTemp('stream_test');
final inputFile = File('${tempDir.path}/input.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');
final decrypted = File('${tempDir.path}/decrypted.bin');

final originalData = Uint8List.fromList(
List.generate(100000, (i) => i % 256),
);
await inputFile.writeAsBytes(originalData);

//generate key and create cipher
final key = await generateAes256GcmKey();
final cipher = await createAes256Gcm(key: key);

//Encrypt
await for (final _ in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipher,
)) {
//wait for completion
}
//Decrypt
await for (final _ in StreamingService.decryptFile(
inputPath: encrypted.path,
outputPath: decrypted.path,
cipher: cipher,
)) {
//wait for completion
}
//verify
final result = await decrypted.readAsBytes();
expect(result, originalData);

//cleanup
await tempDir.delete(recursive: true);
});

test('streaming hash matches one-shot hash', () async {
final tempDir = await Directory.systemTemp.createTemp('hash_test');
final file = File('${tempDir.path}/test.bin');

final data = Uint8List.fromList(List.generate(50000, (i) => i % 256));
await file.writeAsBytes(data);

// streaming hash
final hasher = await createBlake3();
final streamDigest = await StreamingService.hashFile(
filePath: file.path,
hasher: hasher,
);

// one-shot hash
final oneshotDigest = await blake3Hash(data: data);

expect(streamDigest, oneshotDigest);

await tempDir.delete(recursive: true);
});

test('progress reports from 0 to 1', () async {
final tempDir = await Directory.systemTemp.createTemp('progress_test');
final inputFile = File('${tempDir.path}/input.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');

// create 1MB file
final data = Uint8List(1024 * 1024);
await inputFile.writeAsBytes(data);

final key = await generateAes256GcmKey();
final cipher = await createAes256Gcm(key: key);

final progressValues = <double>[];

await for (final progress in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipher,
)) {
progressValues.add(progress);
}

//verify progress goes from ~0 to 1
expect(progressValues.first, lessThan(0.1));
expect(progressValues.last, closeTo(1.0, 0.01));

// verify monotonically increasing
for (int i = 1; i < progressValues.length; i++) {
expect(progressValues[i], greaterThanOrEqualTo(progressValues[i - 1]));
}

await tempDir.delete(recursive: true);
});

test('wrong key fails decryption', () async {
final tempDir = await Directory.systemTemp.createTemp('wrongkey_test');
final inputFile = File('${tempDir.path}/input.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');
final decrypted = File('${tempDir.path}/decrypted.bin');

await inputFile.writeAsBytes(Uint8List.fromList([1, 2, 3, 4, 5]));

final keyA = await generateAes256GcmKey();
final cipherA = await createAes256Gcm(key: keyA);

await for (final _ in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipherA,
)) {}

final keyB = await generateAes256GcmKey();
final cipherB = await createAes256Gcm(key: keyB);

bool errorThrown = false;
try {
await for (final _ in StreamingService.decryptFile(
inputPath: encrypted.path,
outputPath: decrypted.path,
cipher: cipherB,
)) {}
} catch (e) {
errorThrown = true;
}
expect(errorThrown, true);

await tempDir.delete(recursive: true);
});

test('empty file roundtrip', () async {
final tempDir = await Directory.systemTemp.createTemp('empty_test');
final inputFile = File('${tempDir.path}/empty.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');
final decrypted = File('${tempDir.path}/decrypted.bin');

await inputFile.writeAsBytes(Uint8List(0)); // Empty file

final key = await generateAes256GcmKey();
final cipher = await createAes256Gcm(key: key);

await for (final _ in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipher,
)) {}

await for (final _ in StreamingService.decryptFile(
inputPath: encrypted.path,
outputPath: decrypted.path,
cipher: cipher,
)) {}

final result = await decrypted.readAsBytes();
expect(result.length, 0);

await tempDir.delete(recursive: true);
});

test('small file padding stripped correctly', () async {
final tempDir = await Directory.systemTemp.createTemp('padding_test');
final inputFile = File('${tempDir.path}/small.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');
final decrypted = File('${tempDir.path}/decrypted.bin');

final originalData = Uint8List(100); // exactly 100 bytes
await inputFile.writeAsBytes(originalData);

final key = await generateAes256GcmKey();
final cipher = await createAes256Gcm(key: key);

await for (final _ in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipher,
)) {}

await for (final _ in StreamingService.decryptFile(
inputPath: encrypted.path,
outputPath: decrypted.path,
cipher: cipher,
)) {}

final result = await decrypted.readAsBytes();
expect(result.length, 100, reason: 'Padding should be stripped, output should be exactly 100 bytes, not 64KB');

await tempDir.delete(recursive: true);
});

test('encrypted chunks are uniform size', () async {
final tempDir = await Directory.systemTemp.createTemp('uniform_test');
final inputFile = File('${tempDir.path}/input.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');

// Create file that doesn't fill last chunk
final data = Uint8List(150);
await inputFile.writeAsBytes(data);

final key = await generateAes256GcmKey();
final cipher = await createAes256Gcm(key: key);

await for (final _ in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipher,
)) {}

final encryptedSize = await encrypted.length();
const streamHeaderSize = 16;
const encryptedChunkSize = 65564;

final dataPortionSize = encryptedSize - streamHeaderSize;

expect(
dataPortionSize % encryptedChunkSize,
0,
reason: 'All encrypted chunks should be uniform size. Got file size $encryptedSize',
);

await tempDir.delete(recursive: true);
});

test('tampered padding detected end-to-end', () async {
final tempDir = await Directory.systemTemp.createTemp('tamper_test');
final inputFile = File('${tempDir.path}/input.bin');
final encrypted = File('${tempDir.path}/encrypted.bin');
final tampered = File('${tempDir.path}/tampered.bin');
final decrypted = File('${tempDir.path}/decrypted.bin');

final data = Uint8List(100);
await inputFile.writeAsBytes(data);

final key = await generateAes256GcmKey();
final cipher = await createAes256Gcm(key: key);

await for (final _ in StreamingService.encryptFile(
inputPath: inputFile.path,
outputPath: encrypted.path,
cipher: cipher,
)) {}

final encryptedBytes = await encrypted.readAsBytes();
final tamperedBytes = Uint8List.fromList(encryptedBytes);
tamperedBytes[tamperedBytes.length - 100] ^= 0xFF;
await tampered.writeAsBytes(tamperedBytes);

await expectLater(
Future(() async {
await for (final _ in StreamingService.decryptFile(
inputPath: tampered.path,
outputPath: decrypted.path,
cipher: cipher,
)) {}
}),
throwsA(anything),
reason: 'Tampered padding should be detected and throw error',
);

await tempDir.delete(recursive: true);
});

});
}
2 changes: 2 additions & 0 deletions flutter_rust_bridge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust
enable_lifetime: true
rust_features:
- compression
Loading
Loading