-
Notifications
You must be signed in to change notification settings - Fork 29
perf: run media sanitizing and file crypto off the main isolate #703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,17 +91,12 @@ class EncryptedImageUploadService { | |
| ); | ||
|
|
||
| // 3. Encrypt with ChaCha20-Poly1305 | ||
| final encryptionResult = EncryptionService.encryptChaCha20Poly1305( | ||
| final encryptedBlob = await EncryptionService.encryptToBlobAsync( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This replacement likewise eliminates Useful? React with 👍 / 👎. |
||
| key: sharedKey, | ||
| plaintext: validationResult.validatedData, | ||
| ); | ||
|
|
||
| final encryptedBlob = encryptionResult.toBlob(); | ||
| logger.i( | ||
| '🔐 Image encrypted successfully: ${encryptedBlob.length} bytes ' | ||
| '(nonce: ${encryptionResult.nonce.length}B, ' | ||
| 'data: ${encryptionResult.encryptedData.length}B, ' | ||
| 'tag: ${encryptionResult.authTag.length}B)' | ||
| ); | ||
|
|
||
| // 4. Upload encrypted blob to Blossom | ||
|
|
@@ -151,7 +146,7 @@ class EncryptedImageUploadService { | |
| logger.i('📥 Downloaded encrypted blob: ${encryptedBlob.length} bytes'); | ||
|
|
||
| // 2. Decrypt with ChaCha20-Poly1305 | ||
| final decryptedImage = EncryptionService.decryptFromBlob( | ||
| final decryptedImage = await EncryptionService.decryptFromBlobAsync( | ||
| key: sharedKey, | ||
| blob: encryptedBlob, | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import 'dart:typed_data'; | ||
|
|
||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:image/image.dart' as img; | ||
| import 'package:mostro_mobile/services/encryption_service.dart'; | ||
| import 'package:mostro_mobile/services/media_validation_service.dart'; | ||
|
|
||
| /// Image sanitizing (pure-Dart decode + re-encode: 1-5 s for a phone photo) | ||
| /// and whole-file ChaCha20-Poly1305 ran on the main isolate, freezing the UI | ||
| /// while sending or opening media. Both now run through Isolate.run; these | ||
| /// tests pin the async variants' behaviour, including error propagation | ||
| /// across the isolate boundary. | ||
| void main() { | ||
| final key = Uint8List.fromList(List.generate(32, (i) => i)); | ||
|
|
||
| Uint8List tinyPng() { | ||
| final image = img.Image(width: 2, height: 2); | ||
| img.fill(image, color: img.ColorRgb8(200, 50, 50)); | ||
| return Uint8List.fromList(img.encodePng(image)); | ||
| } | ||
|
|
||
| group('EncryptionService isolate variants', () { | ||
| test('async blob roundtrip matches the sync implementation', () async { | ||
| final plaintext = Uint8List.fromList(List.generate(1024, (i) => i % 251)); | ||
|
|
||
| final blob = await EncryptionService.encryptToBlobAsync( | ||
| key: key, | ||
| plaintext: plaintext, | ||
| ); | ||
| final decrypted = await EncryptionService.decryptFromBlobAsync( | ||
| key: key, | ||
| blob: blob, | ||
| ); | ||
|
|
||
| expect(decrypted, plaintext); | ||
| // Cross-compatibility: sync decrypt reads the async-produced blob. | ||
| expect(EncryptionService.decryptFromBlob(key: key, blob: blob), | ||
| plaintext); | ||
| }); | ||
|
|
||
| test('a tampered blob fails across the isolate boundary', () async { | ||
| final blob = await EncryptionService.encryptToBlobAsync( | ||
| key: key, | ||
| plaintext: Uint8List.fromList([1, 2, 3]), | ||
| ); | ||
| blob[blob.length - 1] ^= 0xFF; | ||
|
|
||
| await expectLater( | ||
| EncryptionService.decryptFromBlobAsync(key: key, blob: blob), | ||
| throwsA(isA<Exception>()), | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| group('MediaValidationService off the main isolate', () { | ||
| test('light sanitization still validates and strips a PNG', () async { | ||
| final result = | ||
| await MediaValidationService.validateAndSanitizeImageLight(tinyPng()); | ||
|
|
||
| expect(result.mimeType, 'image/png'); | ||
| expect(result.width, 2); | ||
| expect(result.height, 2); | ||
| expect(img.decodePng(result.validatedData), isNotNull); | ||
| }); | ||
|
|
||
| test('heavy sanitization still validates a PNG', () async { | ||
| final result = | ||
| await MediaValidationService.validateAndSanitizeImage(tinyPng()); | ||
|
|
||
| expect(result.mimeType, 'image/png'); | ||
| expect(img.decodePng(result.validatedData), isNotNull); | ||
| }); | ||
|
|
||
| test('garbage input propagates the validation error', () async { | ||
| await expectLater( | ||
| MediaValidationService.validateAndSanitizeImageLight( | ||
| Uint8List.fromList(List.filled(64, 7)), | ||
| ), | ||
| throwsA(isA<Exception>()), | ||
| ); | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replacement removes the only declaration of
encryptionResult, but the result construction below still evaluates_bytesToHex(encryptionResult.nonce). Any build that includes this service therefore fails with an undefined identifier, preventing encrypted file uploads from compiling; derive the nonce from the blob prefix or return it alongside the blob from the async API.Useful? React with 👍 / 👎.