Skip to content

Commit c35d7ff

Browse files
authored
Merge pull request #48 from MicroClub-USTHB/streaming
Add streaming encrypt decrypt and hash pipeline
2 parents 09e5195 + 060d275 commit c35d7ff

24 files changed

Lines changed: 2705 additions & 2850 deletions
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import 'dart:io';
2+
import 'dart:typed_data';
3+
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:integration_test/integration_test.dart';
6+
import 'package:m_security/src/rust/api/encryption.dart';
7+
import 'package:m_security/src/rust/frb_generated.dart';
8+
import 'package:m_security/src/streaming/streaming_service.dart';
9+
import 'package:m_security/src/rust/api/hashing.dart';
10+
11+
void main() {
12+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
13+
setUpAll(() async => await RustLib.init());
14+
15+
group('Streaming', () {
16+
test('encrypt then decrypt file roundtrip', () async {
17+
//Create temp file with known content
18+
//→ encrypt → decrypt → compare bytes identical
19+
final tempDir = await Directory.systemTemp.createTemp('stream_test');
20+
final inputFile = File('${tempDir.path}/input.bin');
21+
final encrypted = File('${tempDir.path}/encrypted.bin');
22+
final decrypted = File('${tempDir.path}/decrypted.bin');
23+
24+
final originalData = Uint8List.fromList(
25+
List.generate(100000, (i) => i % 256),
26+
);
27+
await inputFile.writeAsBytes(originalData);
28+
29+
//generate key and create cipher
30+
final key = await generateAes256GcmKey();
31+
final cipher = await createAes256Gcm(key: key);
32+
33+
//Encrypt
34+
await for (final _ in StreamingService.encryptFile(
35+
inputPath: inputFile.path,
36+
outputPath: encrypted.path,
37+
cipher: cipher,
38+
)) {
39+
//wait for completion
40+
}
41+
//Decrypt
42+
await for (final _ in StreamingService.decryptFile(
43+
inputPath: encrypted.path,
44+
outputPath: decrypted.path,
45+
cipher: cipher,
46+
)) {
47+
//wait for completion
48+
}
49+
//verify
50+
final result = await decrypted.readAsBytes();
51+
expect(result, originalData);
52+
53+
//cleanup
54+
await tempDir.delete(recursive: true);
55+
});
56+
57+
test('streaming hash matches one-shot hash', () async {
58+
final tempDir = await Directory.systemTemp.createTemp('hash_test');
59+
final file = File('${tempDir.path}/test.bin');
60+
61+
final data = Uint8List.fromList(List.generate(50000, (i) => i % 256));
62+
await file.writeAsBytes(data);
63+
64+
// streaming hash
65+
final hasher = await createBlake3();
66+
final streamDigest = await StreamingService.hashFile(
67+
filePath: file.path,
68+
hasher: hasher,
69+
);
70+
71+
// one-shot hash
72+
final oneshotDigest = await blake3Hash(data: data);
73+
74+
expect(streamDigest, oneshotDigest);
75+
76+
await tempDir.delete(recursive: true);
77+
});
78+
79+
test('progress reports from 0 to 1', () async {
80+
final tempDir = await Directory.systemTemp.createTemp('progress_test');
81+
final inputFile = File('${tempDir.path}/input.bin');
82+
final encrypted = File('${tempDir.path}/encrypted.bin');
83+
84+
// create 1MB file
85+
final data = Uint8List(1024 * 1024);
86+
await inputFile.writeAsBytes(data);
87+
88+
final key = await generateAes256GcmKey();
89+
final cipher = await createAes256Gcm(key: key);
90+
91+
final progressValues = <double>[];
92+
93+
await for (final progress in StreamingService.encryptFile(
94+
inputPath: inputFile.path,
95+
outputPath: encrypted.path,
96+
cipher: cipher,
97+
)) {
98+
progressValues.add(progress);
99+
}
100+
101+
//verify progress goes from ~0 to 1
102+
expect(progressValues.first, lessThan(0.1));
103+
expect(progressValues.last, closeTo(1.0, 0.01));
104+
105+
// verify monotonically increasing
106+
for (int i = 1; i < progressValues.length; i++) {
107+
expect(progressValues[i], greaterThanOrEqualTo(progressValues[i - 1]));
108+
}
109+
110+
await tempDir.delete(recursive: true);
111+
});
112+
113+
test('wrong key fails decryption', () async {
114+
final tempDir = await Directory.systemTemp.createTemp('wrongkey_test');
115+
final inputFile = File('${tempDir.path}/input.bin');
116+
final encrypted = File('${tempDir.path}/encrypted.bin');
117+
final decrypted = File('${tempDir.path}/decrypted.bin');
118+
119+
await inputFile.writeAsBytes(Uint8List.fromList([1, 2, 3, 4, 5]));
120+
121+
final keyA = await generateAes256GcmKey();
122+
final cipherA = await createAes256Gcm(key: keyA);
123+
124+
await for (final _ in StreamingService.encryptFile(
125+
inputPath: inputFile.path,
126+
outputPath: encrypted.path,
127+
cipher: cipherA,
128+
)) {}
129+
130+
final keyB = await generateAes256GcmKey();
131+
final cipherB = await createAes256Gcm(key: keyB);
132+
133+
bool errorThrown = false;
134+
try {
135+
await for (final _ in StreamingService.decryptFile(
136+
inputPath: encrypted.path,
137+
outputPath: decrypted.path,
138+
cipher: cipherB,
139+
)) {}
140+
} catch (e) {
141+
errorThrown = true;
142+
}
143+
expect(errorThrown, true);
144+
145+
await tempDir.delete(recursive: true);
146+
});
147+
148+
test('empty file roundtrip', () async {
149+
final tempDir = await Directory.systemTemp.createTemp('empty_test');
150+
final inputFile = File('${tempDir.path}/empty.bin');
151+
final encrypted = File('${tempDir.path}/encrypted.bin');
152+
final decrypted = File('${tempDir.path}/decrypted.bin');
153+
154+
await inputFile.writeAsBytes(Uint8List(0)); // Empty file
155+
156+
final key = await generateAes256GcmKey();
157+
final cipher = await createAes256Gcm(key: key);
158+
159+
await for (final _ in StreamingService.encryptFile(
160+
inputPath: inputFile.path,
161+
outputPath: encrypted.path,
162+
cipher: cipher,
163+
)) {}
164+
165+
await for (final _ in StreamingService.decryptFile(
166+
inputPath: encrypted.path,
167+
outputPath: decrypted.path,
168+
cipher: cipher,
169+
)) {}
170+
171+
final result = await decrypted.readAsBytes();
172+
expect(result.length, 0);
173+
174+
await tempDir.delete(recursive: true);
175+
});
176+
177+
test('small file padding stripped correctly', () async {
178+
final tempDir = await Directory.systemTemp.createTemp('padding_test');
179+
final inputFile = File('${tempDir.path}/small.bin');
180+
final encrypted = File('${tempDir.path}/encrypted.bin');
181+
final decrypted = File('${tempDir.path}/decrypted.bin');
182+
183+
final originalData = Uint8List(100); // exactly 100 bytes
184+
await inputFile.writeAsBytes(originalData);
185+
186+
final key = await generateAes256GcmKey();
187+
final cipher = await createAes256Gcm(key: key);
188+
189+
await for (final _ in StreamingService.encryptFile(
190+
inputPath: inputFile.path,
191+
outputPath: encrypted.path,
192+
cipher: cipher,
193+
)) {}
194+
195+
await for (final _ in StreamingService.decryptFile(
196+
inputPath: encrypted.path,
197+
outputPath: decrypted.path,
198+
cipher: cipher,
199+
)) {}
200+
201+
final result = await decrypted.readAsBytes();
202+
expect(result.length, 100, reason: 'Padding should be stripped, output should be exactly 100 bytes, not 64KB');
203+
204+
await tempDir.delete(recursive: true);
205+
});
206+
207+
test('encrypted chunks are uniform size', () async {
208+
final tempDir = await Directory.systemTemp.createTemp('uniform_test');
209+
final inputFile = File('${tempDir.path}/input.bin');
210+
final encrypted = File('${tempDir.path}/encrypted.bin');
211+
212+
// Create file that doesn't fill last chunk
213+
final data = Uint8List(150);
214+
await inputFile.writeAsBytes(data);
215+
216+
final key = await generateAes256GcmKey();
217+
final cipher = await createAes256Gcm(key: key);
218+
219+
await for (final _ in StreamingService.encryptFile(
220+
inputPath: inputFile.path,
221+
outputPath: encrypted.path,
222+
cipher: cipher,
223+
)) {}
224+
225+
final encryptedSize = await encrypted.length();
226+
const streamHeaderSize = 16;
227+
const encryptedChunkSize = 65564;
228+
229+
final dataPortionSize = encryptedSize - streamHeaderSize;
230+
231+
expect(
232+
dataPortionSize % encryptedChunkSize,
233+
0,
234+
reason: 'All encrypted chunks should be uniform size. Got file size $encryptedSize',
235+
);
236+
237+
await tempDir.delete(recursive: true);
238+
});
239+
240+
test('tampered padding detected end-to-end', () async {
241+
final tempDir = await Directory.systemTemp.createTemp('tamper_test');
242+
final inputFile = File('${tempDir.path}/input.bin');
243+
final encrypted = File('${tempDir.path}/encrypted.bin');
244+
final tampered = File('${tempDir.path}/tampered.bin');
245+
final decrypted = File('${tempDir.path}/decrypted.bin');
246+
247+
final data = Uint8List(100);
248+
await inputFile.writeAsBytes(data);
249+
250+
final key = await generateAes256GcmKey();
251+
final cipher = await createAes256Gcm(key: key);
252+
253+
await for (final _ in StreamingService.encryptFile(
254+
inputPath: inputFile.path,
255+
outputPath: encrypted.path,
256+
cipher: cipher,
257+
)) {}
258+
259+
final encryptedBytes = await encrypted.readAsBytes();
260+
final tamperedBytes = Uint8List.fromList(encryptedBytes);
261+
tamperedBytes[tamperedBytes.length - 100] ^= 0xFF;
262+
await tampered.writeAsBytes(tamperedBytes);
263+
264+
await expectLater(
265+
Future(() async {
266+
await for (final _ in StreamingService.decryptFile(
267+
inputPath: tampered.path,
268+
outputPath: decrypted.path,
269+
cipher: cipher,
270+
)) {}
271+
}),
272+
throwsA(anything),
273+
reason: 'Tampered padding should be detected and throw error',
274+
);
275+
276+
await tempDir.delete(recursive: true);
277+
});
278+
279+
});
280+
}

0 commit comments

Comments
 (0)