diff --git a/example/integration_test/streaming_test.dart b/example/integration_test/streaming_test.dart new file mode 100644 index 0000000..042bd46 --- /dev/null +++ b/example/integration_test/streaming_test.dart @@ -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 = []; + + 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); + }); + + }); +} diff --git a/flutter_rust_bridge.yaml b/flutter_rust_bridge.yaml index e9a8a17..c9f390c 100644 --- a/flutter_rust_bridge.yaml +++ b/flutter_rust_bridge.yaml @@ -2,3 +2,5 @@ rust_input: crate::api rust_root: rust/ dart_output: lib/src/rust enable_lifetime: true +rust_features: + - compression diff --git a/integration_test/compression_test.dart b/integration_test/compression_test.dart new file mode 100644 index 0000000..042eabb --- /dev/null +++ b/integration_test/compression_test.dart @@ -0,0 +1,181 @@ +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/compression.dart'; +import 'package:m_security/src/rust/api/encryption.dart'; +import 'package:m_security/src/rust/frb_generated.dart'; +import 'package:m_security/src/compression/compression_service.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() async => await RustLib.init()); + + group('Compression', () { + test('zstd compress-then-encrypt roundtrip', () async { + final tempDir = await Directory.systemTemp.createTemp('zstd_test'); + addTearDown(() => tempDir.delete(recursive: true)); + final input = 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 input.writeAsBytes(originalData); + + final key = await generateAes256GcmKey(); + final cipher = await createAes256Gcm(key: key); + + await for (final _ in CompressionService.compressAndEncryptFile( + inputPath: input.path, + outputPath: encrypted.path, + cipher: cipher, + config: const CompressionConfig(algorithm: CompressionAlgorithm.zstd), + )) {} + + await for (final _ in CompressionService.decryptAndDecompressFile( + inputPath: encrypted.path, + outputPath: decrypted.path, + cipher: cipher, + )) {} + + final result = await decrypted.readAsBytes(); + expect(result, originalData); + }); + + test('brotli compress-then-encrypt roundtrip', () async { + final tempDir = await Directory.systemTemp.createTemp('brotli_test'); + addTearDown(() => tempDir.delete(recursive: true)); + final input = 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 input.writeAsBytes(originalData); + + final key = await generateAes256GcmKey(); + final cipher = await createAes256Gcm(key: key); + + await for (final _ in CompressionService.compressAndEncryptFile( + inputPath: input.path, + outputPath: encrypted.path, + cipher: cipher, + config: const CompressionConfig(algorithm: CompressionAlgorithm.brotli), + )) {} + + await for (final _ in CompressionService.decryptAndDecompressFile( + inputPath: encrypted.path, + outputPath: decrypted.path, + cipher: cipher, + )) {} + + final result = await decrypted.readAsBytes(); + expect(result, originalData); + }); + + test('compression none roundtrips correctly', () async { + final tempDir = await Directory.systemTemp.createTemp('none_test'); + addTearDown(() => tempDir.delete(recursive: true)); + final input = 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(50000, (i) => i % 256), + ); + await input.writeAsBytes(originalData); + + final key = await generateAes256GcmKey(); + final cipher = await createAes256Gcm(key: key); + + await for (final _ in CompressionService.compressAndEncryptFile( + inputPath: input.path, + outputPath: encrypted.path, + cipher: cipher, + config: const CompressionConfig(algorithm: CompressionAlgorithm.none), + )) {} + + await for (final _ in CompressionService.decryptAndDecompressFile( + inputPath: encrypted.path, + outputPath: decrypted.path, + cipher: cipher, + )) {} + + expect(await decrypted.readAsBytes(), originalData); + }); + + test('jpg file auto-skips compression', () async { + final tempDir = await Directory.systemTemp.createTemp('jpg_test'); + addTearDown(() => tempDir.delete(recursive: true)); + // Use .jpg extension to trigger MIME-aware skip + final input = File('${tempDir.path}/photo.jpg'); + final encrypted = File('${tempDir.path}/encrypted.bin'); + final decrypted = File('${tempDir.path}/decrypted.jpg'); + + final originalData = Uint8List.fromList( + List.generate(50000, (i) => i % 256), + ); + await input.writeAsBytes(originalData); + + final key = await generateAes256GcmKey(); + final cipher = await createAes256Gcm(key: key); + + // Even though we request zstd, .jpg should trigger skip + await for (final _ in CompressionService.compressAndEncryptFile( + inputPath: input.path, + outputPath: encrypted.path, + cipher: cipher, + config: const CompressionConfig(algorithm: CompressionAlgorithm.zstd), + )) {} + + await for (final _ in CompressionService.decryptAndDecompressFile( + inputPath: encrypted.path, + outputPath: decrypted.path, + cipher: cipher, + )) {} + + final result = await decrypted.readAsBytes(); + expect(result, originalData); + }); + + test('custom compression level works', () async { + final tempDir = await Directory.systemTemp.createTemp('level_test'); + addTearDown(() => tempDir.delete(recursive: true)); + final input = 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 input.writeAsBytes(originalData); + + final key = await generateAes256GcmKey(); + final cipher = await createAes256Gcm(key: key); + + // Zstd with level 19 (high compression) + await for (final _ in CompressionService.compressAndEncryptFile( + inputPath: input.path, + outputPath: encrypted.path, + cipher: cipher, + config: const CompressionConfig( + algorithm: CompressionAlgorithm.zstd, + level: 19, + ), + )) {} + + await for (final _ in CompressionService.decryptAndDecompressFile( + inputPath: encrypted.path, + outputPath: decrypted.path, + cipher: cipher, + )) {} + + final result = await decrypted.readAsBytes(); + expect(result, originalData); + }); + }); +} diff --git a/integration_test/streaming_test.dart b/integration_test/streaming_test.dart new file mode 100644 index 0000000..042bd46 --- /dev/null +++ b/integration_test/streaming_test.dart @@ -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 = []; + + 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); + }); + + }); +} diff --git a/lib/m_security.dart b/lib/m_security.dart index 2de16b4..5068bc3 100644 --- a/lib/m_security.dart +++ b/lib/m_security.dart @@ -3,3 +3,5 @@ export 'src/encryption/chacha20.dart'; export 'src/hashing/argon2.dart'; export 'src/kdf/hkdf.dart'; export 'src/rust/frb_generated.dart' show RustLib; +export 'src/streaming/streaming_service.dart'; +export 'src/compression/compression_service.dart'; \ No newline at end of file diff --git a/lib/src/compression/compression_service.dart b/lib/src/compression/compression_service.dart new file mode 100644 index 0000000..aee6ed7 --- /dev/null +++ b/lib/src/compression/compression_service.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:m_security/src/rust/api/compression.dart'; +import 'package:m_security/src/rust/api/encryption.dart' as rust_encryption; +import 'package:m_security/src/rust/api/streaming.dart' as rust_streaming; + +/// Compressed streaming file operations (compress+encrypt, decrypt+decompress). +/// +/// Uses stream-compress-then-chunk: input is compressed as a stream, then +/// the compressed bytes are chunked and encrypted. Decompression algorithm +/// is stored in the file header — decrypt needs no config. +class CompressionService { + CompressionService._(); + + /// Compress then encrypt a file. + /// Returns a Stream of progress (0.0 to 1.0). + static Stream compressAndEncryptFile({ + required String inputPath, + required String outputPath, + required rust_encryption.CipherHandle cipher, + CompressionConfig config = const CompressionConfig( + algorithm: CompressionAlgorithm.zstd, + ), + }) { + return _guardedStream(() => rust_streaming.streamCompressEncryptFile( + cipher: cipher, + compression: config, + inputPath: inputPath, + outputPath: outputPath, + )); + } + + /// Decrypt then decompress a file. + /// Algorithm is read from the encrypted file header — no config needed. + static Stream decryptAndDecompressFile({ + required String inputPath, + required String outputPath, + required rust_encryption.CipherHandle cipher, + }) { + return _guardedStream(() => rust_streaming.streamDecryptDecompressFile( + cipher: cipher, + inputPath: inputPath, + outputPath: outputPath, + )); + } + + static Stream _guardedStream(Stream Function() factory) { + final controller = StreamController(); + runZonedGuarded(() { + factory().listen( + controller.add, + onError: controller.addError, + onDone: () { + Future(() { + if (!controller.isClosed) controller.close(); + }); + }, + ); + }, (error, stack) { + if (!controller.isClosed) { + controller.addError(error, stack); + controller.close(); + } + }); + return controller.stream; + } +} diff --git a/lib/src/rust/api/encryption.dart b/lib/src/rust/api/encryption.dart deleted file mode 100644 index ac451e1..0000000 --- a/lib/src/rust/api/encryption.dart +++ /dev/null @@ -1,64 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../core/error.dart'; -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These functions are ignored because they are not marked as `pub`: `new` - -/// Create a noop encryption handle (for testing FRB opaque pattern). -Future createNoopEncryption() => - RustLib.instance.api.crateApiEncryptionCreateNoopEncryption(); - -/// Create an AES-256-GCM cipher handle from a 32-byte key. -Future createAes256Gcm({required List key}) => - RustLib.instance.api.crateApiEncryptionCreateAes256Gcm(key: key); - -/// Generate a random 32-byte key for AES-256-GCM. -Future generateAes256GcmKey() => - RustLib.instance.api.crateApiEncryptionGenerateAes256GcmKey(); - -/// Create a ChaCha20-Poly1305 cipher handle from a 32-byte key. -Future createChacha20Poly1305({required List key}) => - RustLib.instance.api.crateApiEncryptionCreateChacha20Poly1305(key: key); - -/// Generate a random 32-byte key for ChaCha20-Poly1305. -Future generateChacha20Poly1305Key() => - RustLib.instance.api.crateApiEncryptionGenerateChacha20Poly1305Key(); - -/// Encrypt plaintext using the given cipher handle. -/// -/// Empty plaintext is valid — produces an authenticated tag with no ciphertext, -/// useful for authenticate-only use cases with AAD. -Future encrypt({ - required CipherHandle cipher, - required List plaintext, - required List aad, -}) => RustLib.instance.api.crateApiEncryptionEncrypt( - cipher: cipher, - plaintext: plaintext, - aad: aad, -); - -/// Decrypt ciphertext using the given cipher handle. -Future decrypt({ - required CipherHandle cipher, - required List ciphertext, - required List aad, -}) => RustLib.instance.api.crateApiEncryptionDecrypt( - cipher: cipher, - ciphertext: ciphertext, - aad: aad, -); - -/// Get the algorithm identifier for the cipher. -Future encryptionAlgorithmId({required CipherHandle cipher}) => RustLib - .instance - .api - .crateApiEncryptionEncryptionAlgorithmId(cipher: cipher); - -// Rust type: RustOpaqueMoi> -abstract class CipherHandle implements RustOpaqueInterface {} diff --git a/lib/src/rust/api/encryption/aes_gcm.dart b/lib/src/rust/api/encryption/aes_gcm.dart deleted file mode 100644 index 1ac0ac0..0000000 --- a/lib/src/rust/api/encryption/aes_gcm.dart +++ /dev/null @@ -1,15 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../../core/error.dart'; -import '../../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Aes256GcmCipher` -// These functions are ignored (category: IgnoreBecauseOwnerTyShouldIgnore): `algorithm_id`, `decrypt`, `encrypt`, `new` - -/// Generate a random 32-byte AES-256 key. -Future generateAesKey() => - RustLib.instance.api.crateApiEncryptionAesGcmGenerateAesKey(); diff --git a/lib/src/rust/api/encryption/chacha20.dart b/lib/src/rust/api/encryption/chacha20.dart deleted file mode 100644 index aa3fd7a..0000000 --- a/lib/src/rust/api/encryption/chacha20.dart +++ /dev/null @@ -1,15 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../../core/error.dart'; -import '../../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ChaCha20Poly1305Cipher` -// These functions are ignored (category: IgnoreBecauseOwnerTyShouldIgnore): `algorithm_id`, `decrypt`, `encrypt`, `new` - -/// Generate a random 32-byte ChaCha20-Poly1305 key. -Future generateChachaKey() => - RustLib.instance.api.crateApiEncryptionChacha20GenerateChachaKey(); diff --git a/lib/src/rust/api/encryption/noop.dart b/lib/src/rust/api/encryption/noop.dart deleted file mode 100644 index 7a54ef0..0000000 --- a/lib/src/rust/api/encryption/noop.dart +++ /dev/null @@ -1,24 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../../core/error.dart'; -import '../../core/traits.dart'; -import '../../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// Rust type: RustOpaqueMoi> -abstract class NoopEncryption implements RustOpaqueInterface, Encryption { - Future algorithmId(); - - Future decrypt({ - required List ciphertext, - required List aad, - }); - - Future encrypt({ - required List plaintext, - required List aad, - }); -} diff --git a/lib/src/rust/api/kdf/hkdf.dart b/lib/src/rust/api/kdf/hkdf.dart deleted file mode 100644 index d285253..0000000 --- a/lib/src/rust/api/kdf/hkdf.dart +++ /dev/null @@ -1,37 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../../core/error.dart'; -import '../../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -/// One-shot HKDF-SHA256: extract + expand in a single call. -/// Returns `output_len` bytes of derived key material. -Uint8List hkdfDerive({ - required List ikm, - Uint8List? salt, - required List info, - required BigInt outputLen, -}) => RustLib.instance.api.crateApiKdfHkdfHkdfDerive( - ikm: ikm, - salt: salt, - info: info, - outputLen: outputLen, -); - -/// HKDF-Extract: produce a pseudorandom key (PRK) from input key material. -Uint8List hkdfExtract({required List ikm, Uint8List? salt}) => - RustLib.instance.api.crateApiKdfHkdfHkdfExtract(ikm: ikm, salt: salt); - -/// HKDF-Expand: expand a PRK into `output_len` bytes of derived key material. -Future hkdfExpand({ - required List prk, - required List info, - required BigInt outputLen, -}) => RustLib.instance.api.crateApiKdfHkdfHkdfExpand( - prk: prk, - info: info, - outputLen: outputLen, -); diff --git a/lib/src/rust/core/secret.dart b/lib/src/rust/core/secret.dart deleted file mode 100644 index 0c20d8d..0000000 --- a/lib/src/rust/core/secret.dart +++ /dev/null @@ -1,26 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -/// A buffer that securely zeroes its contents when dropped. -/// -/// Use this for all key material to ensure secrets don't linger in memory. -class SecretBuffer { - final Uint8List inner; - - const SecretBuffer({required this.inner}); - - @override - int get hashCode => inner.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SecretBuffer && - runtimeType == other.runtimeType && - inner == other.inner; -} diff --git a/lib/src/rust/core/traits.dart b/lib/src/rust/core/traits.dart deleted file mode 100644 index 1722d6b..0000000 --- a/lib/src/rust/core/traits.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Placeholder for Rust traits exposed via FRB. -// These are opaque on the Dart side — only the Rust implementations matter. - -abstract class Encryption {} - -abstract class Hasher {} - -abstract class Kdf {} diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart deleted file mode 100644 index a3972ab..0000000 --- a/lib/src/rust/frb_generated.dart +++ /dev/null @@ -1,1825 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -import 'api/encryption.dart'; -import 'api/encryption/aes_gcm.dart'; -import 'api/encryption/chacha20.dart'; -import 'api/encryption/noop.dart'; -import 'api/hashing.dart'; -import 'api/hashing/argon2.dart'; -import 'api/kdf/hkdf.dart'; -import 'core/error.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'frb_generated.io.dart' - if (dart.library.js_interop) 'frb_generated.web.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -/// Main entrypoint of the Rust API -class RustLib extends BaseEntrypoint { - @internal - static final instance = RustLib._(); - - RustLib._(); - - /// Initialize flutter_rust_bridge - static Future init({ - RustLibApi? api, - BaseHandler? handler, - ExternalLibrary? externalLibrary, - bool forceSameCodegenVersion = true, - }) async { - await instance.initImpl( - api: api, - handler: handler, - externalLibrary: externalLibrary, - forceSameCodegenVersion: forceSameCodegenVersion, - ); - } - - /// Initialize flutter_rust_bridge in mock mode. - /// No libraries for FFI are loaded. - static void initMock({required RustLibApi api}) { - instance.initMockImpl(api: api); - } - - /// Dispose flutter_rust_bridge - /// - /// The call to this function is optional, since flutter_rust_bridge (and everything else) - /// is automatically disposed when the app stops. - static void dispose() => instance.disposeImpl(); - - @override - ApiImplConstructor get apiImplConstructor => - RustLibApiImpl.new; - - @override - WireConstructor get wireConstructor => - RustLibWire.fromExternalLibrary; - - @override - Future executeRustInitializers() async {} - - @override - ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => - kDefaultExternalLibraryLoaderConfig; - - @override - String get codegenVersion => '2.11.1'; - - @override - int get rustContentHash => -1451637143; - - static const kDefaultExternalLibraryLoaderConfig = - ExternalLibraryLoaderConfig( - stem: 'rust_lib_m_security', - ioDirectory: 'rust/target/release/', - webPrefix: 'pkg/', - ); -} - -abstract class RustLibApi extends BaseApi { - Future crateApiEncryptionNoopNoopEncryptionAlgorithmId({ - required NoopEncryption that, - }); - - Future crateApiEncryptionNoopNoopEncryptionDecrypt({ - required NoopEncryption that, - required List ciphertext, - required List aad, - }); - - Future crateApiEncryptionNoopNoopEncryptionEncrypt({ - required NoopEncryption that, - required List plaintext, - required List aad, - }); - - Future crateApiHashingArgon2Argon2IdHash({ - required String password, - required Argon2Preset preset, - }); - - Future crateApiHashingArgon2Argon2IdHashWithSalt({ - required String password, - required String salt, - required Argon2Preset preset, - }); - - Future crateApiHashingArgon2Argon2IdVerify({ - required String phcHash, - required String password, - }); - - Future crateApiHashingBlake3Hash({required List data}); - - Future crateApiEncryptionCreateAes256Gcm({ - required List key, - }); - - Future crateApiHashingCreateBlake3(); - - Future crateApiEncryptionCreateChacha20Poly1305({ - required List key, - }); - - Future crateApiEncryptionCreateNoopEncryption(); - - Future crateApiHashingCreateSha3(); - - Future crateApiEncryptionDecrypt({ - required CipherHandle cipher, - required List ciphertext, - required List aad, - }); - - Future crateApiEncryptionEncrypt({ - required CipherHandle cipher, - required List plaintext, - required List aad, - }); - - Future crateApiEncryptionEncryptionAlgorithmId({ - required CipherHandle cipher, - }); - - Future crateApiEncryptionGenerateAes256GcmKey(); - - Future crateApiEncryptionAesGcmGenerateAesKey(); - - Future crateApiEncryptionGenerateChacha20Poly1305Key(); - - Future crateApiEncryptionChacha20GenerateChachaKey(); - - Future crateApiHashingHasherAlgorithmId({ - required HasherHandle handle, - }); - - Future crateApiHashingHasherFinalize({ - required HasherHandle handle, - }); - - Future crateApiHashingHasherReset({required HasherHandle handle}); - - Future crateApiHashingHasherUpdate({ - required HasherHandle handle, - required List data, - }); - - Uint8List crateApiKdfHkdfHkdfDerive({ - required List ikm, - Uint8List? salt, - required List info, - required BigInt outputLen, - }); - - Future crateApiKdfHkdfHkdfExpand({ - required List prk, - required List info, - required BigInt outputLen, - }); - - Uint8List crateApiKdfHkdfHkdfExtract({ - required List ikm, - Uint8List? salt, - }); - - Future crateApiHashingSha3Hash({required List data}); - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_CipherHandle; - - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_CipherHandle; - - CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_CipherHandlePtr; - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_HasherHandle; - - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_HasherHandle; - - CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_HasherHandlePtr; - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_NoopEncryption; - - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_NoopEncryption; - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_NoopEncryptionPtr; -} - -class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { - RustLibApiImpl({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @override - Future crateApiEncryptionNoopNoopEncryptionAlgorithmId({ - required NoopEncryption that, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - that, - serializer, - ); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 1, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiEncryptionNoopNoopEncryptionAlgorithmIdConstMeta, - argValues: [that], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionNoopNoopEncryptionAlgorithmIdConstMeta => - const TaskConstMeta( - debugName: "NoopEncryption_algorithm_id", - argNames: ["that"], - ); - - @override - Future crateApiEncryptionNoopNoopEncryptionDecrypt({ - required NoopEncryption that, - required List ciphertext, - required List aad, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - that, - serializer, - ); - sse_encode_list_prim_u_8_loose(ciphertext, serializer); - sse_encode_list_prim_u_8_loose(aad, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 2, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionNoopNoopEncryptionDecryptConstMeta, - argValues: [that, ciphertext, aad], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionNoopNoopEncryptionDecryptConstMeta => - const TaskConstMeta( - debugName: "NoopEncryption_decrypt", - argNames: ["that", "ciphertext", "aad"], - ); - - @override - Future crateApiEncryptionNoopNoopEncryptionEncrypt({ - required NoopEncryption that, - required List plaintext, - required List aad, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - that, - serializer, - ); - sse_encode_list_prim_u_8_loose(plaintext, serializer); - sse_encode_list_prim_u_8_loose(aad, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 3, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionNoopNoopEncryptionEncryptConstMeta, - argValues: [that, plaintext, aad], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionNoopNoopEncryptionEncryptConstMeta => - const TaskConstMeta( - debugName: "NoopEncryption_encrypt", - argNames: ["that", "plaintext", "aad"], - ); - - @override - Future crateApiHashingArgon2Argon2IdHash({ - required String password, - required Argon2Preset preset, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(password, serializer); - sse_encode_argon_2_preset(preset, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 4, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingArgon2Argon2IdHashConstMeta, - argValues: [password, preset], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingArgon2Argon2IdHashConstMeta => - const TaskConstMeta( - debugName: "argon2id_hash", - argNames: ["password", "preset"], - ); - - @override - Future crateApiHashingArgon2Argon2IdHashWithSalt({ - required String password, - required String salt, - required Argon2Preset preset, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(password, serializer); - sse_encode_String(salt, serializer); - sse_encode_argon_2_preset(preset, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 5, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingArgon2Argon2IdHashWithSaltConstMeta, - argValues: [password, salt, preset], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingArgon2Argon2IdHashWithSaltConstMeta => - const TaskConstMeta( - debugName: "argon2id_hash_with_salt", - argNames: ["password", "salt", "preset"], - ); - - @override - Future crateApiHashingArgon2Argon2IdVerify({ - required String phcHash, - required String password, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(phcHash, serializer); - sse_encode_String(password, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 6, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingArgon2Argon2IdVerifyConstMeta, - argValues: [phcHash, password], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingArgon2Argon2IdVerifyConstMeta => - const TaskConstMeta( - debugName: "argon2id_verify", - argNames: ["phcHash", "password"], - ); - - @override - Future crateApiHashingBlake3Hash({required List data}) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(data, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 7, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: null, - ), - constMeta: kCrateApiHashingBlake3HashConstMeta, - argValues: [data], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingBlake3HashConstMeta => - const TaskConstMeta(debugName: "blake3_hash", argNames: ["data"]); - - @override - Future crateApiEncryptionCreateAes256Gcm({ - required List key, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(key, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 8, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionCreateAes256GcmConstMeta, - argValues: [key], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionCreateAes256GcmConstMeta => - const TaskConstMeta(debugName: "create_aes256_gcm", argNames: ["key"]); - - @override - Future crateApiHashingCreateBlake3() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 9, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle, - decodeErrorData: null, - ), - constMeta: kCrateApiHashingCreateBlake3ConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingCreateBlake3ConstMeta => - const TaskConstMeta(debugName: "create_blake3", argNames: []); - - @override - Future crateApiEncryptionCreateChacha20Poly1305({ - required List key, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(key, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 10, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionCreateChacha20Poly1305ConstMeta, - argValues: [key], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionCreateChacha20Poly1305ConstMeta => - const TaskConstMeta( - debugName: "create_chacha20_poly1305", - argNames: ["key"], - ); - - @override - Future crateApiEncryptionCreateNoopEncryption() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 11, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle, - decodeErrorData: null, - ), - constMeta: kCrateApiEncryptionCreateNoopEncryptionConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionCreateNoopEncryptionConstMeta => - const TaskConstMeta(debugName: "create_noop_encryption", argNames: []); - - @override - Future crateApiHashingCreateSha3() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 12, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle, - decodeErrorData: null, - ), - constMeta: kCrateApiHashingCreateSha3ConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingCreateSha3ConstMeta => - const TaskConstMeta(debugName: "create_sha3", argNames: []); - - @override - Future crateApiEncryptionDecrypt({ - required CipherHandle cipher, - required List ciphertext, - required List aad, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - cipher, - serializer, - ); - sse_encode_list_prim_u_8_loose(ciphertext, serializer); - sse_encode_list_prim_u_8_loose(aad, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 13, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionDecryptConstMeta, - argValues: [cipher, ciphertext, aad], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionDecryptConstMeta => const TaskConstMeta( - debugName: "decrypt", - argNames: ["cipher", "ciphertext", "aad"], - ); - - @override - Future crateApiEncryptionEncrypt({ - required CipherHandle cipher, - required List plaintext, - required List aad, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - cipher, - serializer, - ); - sse_encode_list_prim_u_8_loose(plaintext, serializer); - sse_encode_list_prim_u_8_loose(aad, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 14, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionEncryptConstMeta, - argValues: [cipher, plaintext, aad], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionEncryptConstMeta => const TaskConstMeta( - debugName: "encrypt", - argNames: ["cipher", "plaintext", "aad"], - ); - - @override - Future crateApiEncryptionEncryptionAlgorithmId({ - required CipherHandle cipher, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - cipher, - serializer, - ); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 15, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: null, - ), - constMeta: kCrateApiEncryptionEncryptionAlgorithmIdConstMeta, - argValues: [cipher], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionEncryptionAlgorithmIdConstMeta => - const TaskConstMeta( - debugName: "encryption_algorithm_id", - argNames: ["cipher"], - ); - - @override - Future crateApiEncryptionGenerateAes256GcmKey() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 16, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionGenerateAes256GcmKeyConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionGenerateAes256GcmKeyConstMeta => - const TaskConstMeta(debugName: "generate_aes256_gcm_key", argNames: []); - - @override - Future crateApiEncryptionAesGcmGenerateAesKey() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 17, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionAesGcmGenerateAesKeyConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionAesGcmGenerateAesKeyConstMeta => - const TaskConstMeta(debugName: "generate_aes_key", argNames: []); - - @override - Future crateApiEncryptionGenerateChacha20Poly1305Key() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 18, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionGenerateChacha20Poly1305KeyConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionGenerateChacha20Poly1305KeyConstMeta => - const TaskConstMeta( - debugName: "generate_chacha20_poly1305_key", - argNames: [], - ); - - @override - Future crateApiEncryptionChacha20GenerateChachaKey() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 19, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiEncryptionChacha20GenerateChachaKeyConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiEncryptionChacha20GenerateChachaKeyConstMeta => - const TaskConstMeta(debugName: "generate_chacha_key", argNames: []); - - @override - Future crateApiHashingHasherAlgorithmId({ - required HasherHandle handle, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - handle, - serializer, - ); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 20, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingHasherAlgorithmIdConstMeta, - argValues: [handle], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingHasherAlgorithmIdConstMeta => - const TaskConstMeta( - debugName: "hasher_algorithm_id", - argNames: ["handle"], - ); - - @override - Future crateApiHashingHasherFinalize({ - required HasherHandle handle, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - handle, - serializer, - ); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 21, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingHasherFinalizeConstMeta, - argValues: [handle], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingHasherFinalizeConstMeta => - const TaskConstMeta(debugName: "hasher_finalize", argNames: ["handle"]); - - @override - Future crateApiHashingHasherReset({required HasherHandle handle}) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - handle, - serializer, - ); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 22, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingHasherResetConstMeta, - argValues: [handle], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingHasherResetConstMeta => - const TaskConstMeta(debugName: "hasher_reset", argNames: ["handle"]); - - @override - Future crateApiHashingHasherUpdate({ - required HasherHandle handle, - required List data, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - handle, - serializer, - ); - sse_encode_list_prim_u_8_loose(data, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 23, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiHashingHasherUpdateConstMeta, - argValues: [handle, data], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingHasherUpdateConstMeta => - const TaskConstMeta( - debugName: "hasher_update", - argNames: ["handle", "data"], - ); - - @override - Uint8List crateApiKdfHkdfHkdfDerive({ - required List ikm, - Uint8List? salt, - required List info, - required BigInt outputLen, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(ikm, serializer); - sse_encode_opt_list_prim_u_8_strict(salt, serializer); - sse_encode_list_prim_u_8_loose(info, serializer); - sse_encode_usize(outputLen, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiKdfHkdfHkdfDeriveConstMeta, - argValues: [ikm, salt, info, outputLen], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiKdfHkdfHkdfDeriveConstMeta => const TaskConstMeta( - debugName: "hkdf_derive", - argNames: ["ikm", "salt", "info", "outputLen"], - ); - - @override - Future crateApiKdfHkdfHkdfExpand({ - required List prk, - required List info, - required BigInt outputLen, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(prk, serializer); - sse_encode_list_prim_u_8_loose(info, serializer); - sse_encode_usize(outputLen, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 25, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiKdfHkdfHkdfExpandConstMeta, - argValues: [prk, info, outputLen], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiKdfHkdfHkdfExpandConstMeta => const TaskConstMeta( - debugName: "hkdf_expand", - argNames: ["prk", "info", "outputLen"], - ); - - @override - Uint8List crateApiKdfHkdfHkdfExtract({ - required List ikm, - Uint8List? salt, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(ikm, serializer); - sse_encode_opt_list_prim_u_8_strict(salt, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: sse_decode_crypto_error, - ), - constMeta: kCrateApiKdfHkdfHkdfExtractConstMeta, - argValues: [ikm, salt], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiKdfHkdfHkdfExtractConstMeta => - const TaskConstMeta(debugName: "hkdf_extract", argNames: ["ikm", "salt"]); - - @override - Future crateApiHashingSha3Hash({required List data}) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(data, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 27, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, - decodeErrorData: null, - ), - constMeta: kCrateApiHashingSha3HashConstMeta, - argValues: [data], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiHashingSha3HashConstMeta => - const TaskConstMeta(debugName: "sha3_hash", argNames: ["data"]); - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_CipherHandle => wire - .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle; - - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_CipherHandle => wire - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle; - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_HasherHandle => wire - .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle; - - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_HasherHandle => wire - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle; - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_NoopEncryption => wire - .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption; - - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_NoopEncryption => wire - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption; - - @protected - CipherHandle - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return CipherHandleImpl.frbInternalDcoDecode(raw as List); - } - - @protected - HasherHandle - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return HasherHandleImpl.frbInternalDcoDecode(raw as List); - } - - @protected - NoopEncryption - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return NoopEncryptionImpl.frbInternalDcoDecode(raw as List); - } - - @protected - CipherHandle - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return CipherHandleImpl.frbInternalDcoDecode(raw as List); - } - - @protected - HasherHandle - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return HasherHandleImpl.frbInternalDcoDecode(raw as List); - } - - @protected - NoopEncryption - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return NoopEncryptionImpl.frbInternalDcoDecode(raw as List); - } - - @protected - CipherHandle - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return CipherHandleImpl.frbInternalDcoDecode(raw as List); - } - - @protected - HasherHandle - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return HasherHandleImpl.frbInternalDcoDecode(raw as List); - } - - @protected - NoopEncryption - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return NoopEncryptionImpl.frbInternalDcoDecode(raw as List); - } - - @protected - String dco_decode_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as String; - } - - @protected - Argon2Preset dco_decode_argon_2_preset(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return Argon2Preset.values[raw as int]; - } - - @protected - CryptoError dco_decode_crypto_error(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - switch (raw[0]) { - case 0: - return CryptoError_InvalidKeyLength( - expected: dco_decode_usize(raw[1]), - actual: dco_decode_usize(raw[2]), - ); - case 1: - return CryptoError_InvalidNonce(); - case 2: - return CryptoError_EncryptionFailed(dco_decode_String(raw[1])); - case 3: - return CryptoError_DecryptionFailed(); - case 4: - return CryptoError_HashingFailed(dco_decode_String(raw[1])); - case 5: - return CryptoError_KdfFailed(dco_decode_String(raw[1])); - case 6: - return CryptoError_IoError(dco_decode_String(raw[1])); - case 7: - return CryptoError_InvalidParameter(dco_decode_String(raw[1])); - case 8: - return CryptoError_AuthenticationFailed(); - default: - throw Exception("unreachable"); - } - } - - @protected - int dco_decode_i_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - List dco_decode_list_prim_u_8_loose(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as List; - } - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint8List; - } - - @protected - Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_list_prim_u_8_strict(raw); - } - - @protected - int dco_decode_u_8(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - void dco_decode_unit(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return; - } - - @protected - BigInt dco_decode_usize(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dcoDecodeU64(raw); - } - - @protected - CipherHandle - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return CipherHandleImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - HasherHandle - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return HasherHandleImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - NoopEncryption - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return NoopEncryptionImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - CipherHandle - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return CipherHandleImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - HasherHandle - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return HasherHandleImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - NoopEncryption - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return NoopEncryptionImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - CipherHandle - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return CipherHandleImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - HasherHandle - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return HasherHandleImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - NoopEncryption - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return NoopEncryptionImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); - } - - @protected - String sse_decode_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_prim_u_8_strict(deserializer); - return utf8.decoder.convert(inner); - } - - @protected - Argon2Preset sse_decode_argon_2_preset(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_i_32(deserializer); - return Argon2Preset.values[inner]; - } - - @protected - CryptoError sse_decode_crypto_error(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var tag_ = sse_decode_i_32(deserializer); - switch (tag_) { - case 0: - var var_expected = sse_decode_usize(deserializer); - var var_actual = sse_decode_usize(deserializer); - return CryptoError_InvalidKeyLength( - expected: var_expected, - actual: var_actual, - ); - case 1: - return CryptoError_InvalidNonce(); - case 2: - var var_field0 = sse_decode_String(deserializer); - return CryptoError_EncryptionFailed(var_field0); - case 3: - return CryptoError_DecryptionFailed(); - case 4: - var var_field0 = sse_decode_String(deserializer); - return CryptoError_HashingFailed(var_field0); - case 5: - var var_field0 = sse_decode_String(deserializer); - return CryptoError_KdfFailed(var_field0); - case 6: - var var_field0 = sse_decode_String(deserializer); - return CryptoError_IoError(var_field0); - case 7: - var var_field0 = sse_decode_String(deserializer); - return CryptoError_InvalidParameter(var_field0); - case 8: - return CryptoError_AuthenticationFailed(); - default: - throw UnimplementedError(''); - } - } - - @protected - int sse_decode_i_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getInt32(); - } - - @protected - List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint8List(len_); - } - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint8List(len_); - } - - @protected - Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_list_prim_u_8_strict(deserializer)); - } else { - return null; - } - } - - @protected - int sse_decode_u_8(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8(); - } - - @protected - void sse_decode_unit(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - } - - @protected - BigInt sse_decode_usize(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getBigUint64(); - } - - @protected - bool sse_decode_bool(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8() != 0; - } - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as CipherHandleImpl).frbInternalSseEncode(move: true), - serializer, - ); - } - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as HasherHandleImpl).frbInternalSseEncode(move: true), - serializer, - ); - } - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as NoopEncryptionImpl).frbInternalSseEncode(move: true), - serializer, - ); - } - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as CipherHandleImpl).frbInternalSseEncode(move: false), - serializer, - ); - } - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as HasherHandleImpl).frbInternalSseEncode(move: false), - serializer, - ); - } - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as NoopEncryptionImpl).frbInternalSseEncode(move: false), - serializer, - ); - } - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as CipherHandleImpl).frbInternalSseEncode(move: null), - serializer, - ); - } - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as HasherHandleImpl).frbInternalSseEncode(move: null), - serializer, - ); - } - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as NoopEncryptionImpl).frbInternalSseEncode(move: null), - serializer, - ); - } - - @protected - void sse_encode_String(String self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); - } - - @protected - void sse_encode_argon_2_preset(Argon2Preset self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.index, serializer); - } - - @protected - void sse_encode_crypto_error(CryptoError self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - switch (self) { - case CryptoError_InvalidKeyLength( - expected: final expected, - actual: final actual, - ): - sse_encode_i_32(0, serializer); - sse_encode_usize(expected, serializer); - sse_encode_usize(actual, serializer); - case CryptoError_InvalidNonce(): - sse_encode_i_32(1, serializer); - case CryptoError_EncryptionFailed(field0: final field0): - sse_encode_i_32(2, serializer); - sse_encode_String(field0, serializer); - case CryptoError_DecryptionFailed(): - sse_encode_i_32(3, serializer); - case CryptoError_HashingFailed(field0: final field0): - sse_encode_i_32(4, serializer); - sse_encode_String(field0, serializer); - case CryptoError_KdfFailed(field0: final field0): - sse_encode_i_32(5, serializer); - sse_encode_String(field0, serializer); - case CryptoError_IoError(field0: final field0): - sse_encode_i_32(6, serializer); - sse_encode_String(field0, serializer); - case CryptoError_InvalidParameter(field0: final field0): - sse_encode_i_32(7, serializer); - sse_encode_String(field0, serializer); - case CryptoError_AuthenticationFailed(): - sse_encode_i_32(8, serializer); - } - } - - @protected - void sse_encode_i_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putInt32(self); - } - - @protected - void sse_encode_list_prim_u_8_loose( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint8List( - self is Uint8List ? self : Uint8List.fromList(self), - ); - } - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint8List(self); - } - - @protected - void sse_encode_opt_list_prim_u_8_strict( - Uint8List? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_list_prim_u_8_strict(self, serializer); - } - } - - @protected - void sse_encode_u_8(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self); - } - - @protected - void sse_encode_unit(void self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - } - - @protected - void sse_encode_usize(BigInt self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putBigUint64(self); - } - - @protected - void sse_encode_bool(bool self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self ? 1 : 0); - } -} - -@sealed -class CipherHandleImpl extends RustOpaque implements CipherHandle { - // Not to be used by end users - CipherHandleImpl.frbInternalDcoDecode(List wire) - : super.frbInternalDcoDecode(wire, _kStaticData); - - // Not to be used by end users - CipherHandleImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) - : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); - - static final _kStaticData = RustArcStaticData( - rustArcIncrementStrongCount: - RustLib.instance.api.rust_arc_increment_strong_count_CipherHandle, - rustArcDecrementStrongCount: - RustLib.instance.api.rust_arc_decrement_strong_count_CipherHandle, - rustArcDecrementStrongCountPtr: - RustLib.instance.api.rust_arc_decrement_strong_count_CipherHandlePtr, - ); -} - -@sealed -class HasherHandleImpl extends RustOpaque implements HasherHandle { - // Not to be used by end users - HasherHandleImpl.frbInternalDcoDecode(List wire) - : super.frbInternalDcoDecode(wire, _kStaticData); - - // Not to be used by end users - HasherHandleImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) - : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); - - static final _kStaticData = RustArcStaticData( - rustArcIncrementStrongCount: - RustLib.instance.api.rust_arc_increment_strong_count_HasherHandle, - rustArcDecrementStrongCount: - RustLib.instance.api.rust_arc_decrement_strong_count_HasherHandle, - rustArcDecrementStrongCountPtr: - RustLib.instance.api.rust_arc_decrement_strong_count_HasherHandlePtr, - ); -} - -@sealed -class NoopEncryptionImpl extends RustOpaque implements NoopEncryption { - // Not to be used by end users - NoopEncryptionImpl.frbInternalDcoDecode(List wire) - : super.frbInternalDcoDecode(wire, _kStaticData); - - // Not to be used by end users - NoopEncryptionImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) - : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); - - static final _kStaticData = RustArcStaticData( - rustArcIncrementStrongCount: - RustLib.instance.api.rust_arc_increment_strong_count_NoopEncryption, - rustArcDecrementStrongCount: - RustLib.instance.api.rust_arc_decrement_strong_count_NoopEncryption, - rustArcDecrementStrongCountPtr: - RustLib.instance.api.rust_arc_decrement_strong_count_NoopEncryptionPtr, - ); - - Future algorithmId() => RustLib.instance.api - .crateApiEncryptionNoopNoopEncryptionAlgorithmId(that: this); - - Future decrypt({ - required List ciphertext, - required List aad, - }) => RustLib.instance.api.crateApiEncryptionNoopNoopEncryptionDecrypt( - that: this, - ciphertext: ciphertext, - aad: aad, - ); - - Future encrypt({ - required List plaintext, - required List aad, - }) => RustLib.instance.api.crateApiEncryptionNoopNoopEncryptionEncrypt( - that: this, - plaintext: plaintext, - aad: aad, - ); -} diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart deleted file mode 100644 index 39fe55b..0000000 --- a/lib/src/rust/frb_generated.io.dart +++ /dev/null @@ -1,429 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -import 'api/encryption.dart'; -import 'api/encryption/aes_gcm.dart'; -import 'api/encryption/chacha20.dart'; -import 'api/encryption/noop.dart'; -import 'api/hashing.dart'; -import 'api/hashing/argon2.dart'; -import 'api/kdf/hkdf.dart'; -import 'core/error.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi' as ffi; -import 'frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; - -abstract class RustLibApiImplPlatform extends BaseApiImpl { - RustLibApiImplPlatform({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_CipherHandlePtr => wire - ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandlePtr; - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_HasherHandlePtr => wire - ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandlePtr; - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_NoopEncryptionPtr => wire - ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryptionPtr; - - @protected - CipherHandle - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ); - - @protected - HasherHandle - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ); - - @protected - NoopEncryption - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ); - - @protected - CipherHandle - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ); - - @protected - HasherHandle - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ); - - @protected - NoopEncryption - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ); - - @protected - CipherHandle - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ); - - @protected - HasherHandle - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ); - - @protected - NoopEncryption - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ); - - @protected - String dco_decode_String(dynamic raw); - - @protected - Argon2Preset dco_decode_argon_2_preset(dynamic raw); - - @protected - CryptoError dco_decode_crypto_error(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - List dco_decode_list_prim_u_8_loose(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); - - @protected - Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); - - @protected - int dco_decode_u_8(dynamic raw); - - @protected - void dco_decode_unit(dynamic raw); - - @protected - BigInt dco_decode_usize(dynamic raw); - - @protected - CipherHandle - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ); - - @protected - HasherHandle - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ); - - @protected - NoopEncryption - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ); - - @protected - CipherHandle - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ); - - @protected - HasherHandle - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ); - - @protected - NoopEncryption - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ); - - @protected - CipherHandle - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ); - - @protected - HasherHandle - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ); - - @protected - NoopEncryption - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - Argon2Preset sse_decode_argon_2_preset(SseDeserializer deserializer); - - @protected - CryptoError sse_decode_crypto_error(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - int sse_decode_u_8(SseDeserializer deserializer); - - @protected - void sse_decode_unit(SseDeserializer deserializer); - - @protected - BigInt sse_decode_usize(SseDeserializer deserializer); - - @protected - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_argon_2_preset(Argon2Preset self, SseSerializer serializer); - - @protected - void sse_encode_crypto_error(CryptoError self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_list_prim_u_8_strict( - Uint8List? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_u_8(int self, SseSerializer serializer); - - @protected - void sse_encode_unit(void self, SseSerializer serializer); - - @protected - void sse_encode_usize(BigInt self, SseSerializer serializer); - - @protected - void sse_encode_bool(bool self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => - RustLibWire(lib.ffiDynamicLibrary); - - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - RustLibWire(ffi.DynamicLibrary dynamicLibrary) - : _lookup = dynamicLibrary.lookup; - - void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - ffi.Pointer ptr, - ) { - return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - ptr, - ); - } - - late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandlePtr = - _lookup)>>( - 'frbgen_m_security_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle', - ); - late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle = - _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandlePtr - .asFunction)>(); - - void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - ffi.Pointer ptr, - ) { - return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - ptr, - ); - } - - late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandlePtr = - _lookup)>>( - 'frbgen_m_security_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle', - ); - late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle = - _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandlePtr - .asFunction)>(); - - void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - ffi.Pointer ptr, - ) { - return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - ptr, - ); - } - - late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandlePtr = - _lookup)>>( - 'frbgen_m_security_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle', - ); - late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle = - _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandlePtr - .asFunction)>(); - - void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - ffi.Pointer ptr, - ) { - return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - ptr, - ); - } - - late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandlePtr = - _lookup)>>( - 'frbgen_m_security_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle', - ); - late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle = - _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandlePtr - .asFunction)>(); - - void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ffi.Pointer ptr, - ) { - return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr, - ); - } - - late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryptionPtr = - _lookup)>>( - 'frbgen_m_security_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption', - ); - late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption = - _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryptionPtr - .asFunction)>(); - - void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ffi.Pointer ptr, - ) { - return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr, - ); - } - - late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryptionPtr = - _lookup)>>( - 'frbgen_m_security_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption', - ); - late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption = - _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryptionPtr - .asFunction)>(); -} diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart deleted file mode 100644 index f87f370..0000000 --- a/lib/src/rust/frb_generated.web.dart +++ /dev/null @@ -1,405 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -// Static analysis wrongly picks the IO variant, thus ignore this -// ignore_for_file: argument_type_not_assignable - -import 'api/encryption.dart'; -import 'api/encryption/aes_gcm.dart'; -import 'api/encryption/chacha20.dart'; -import 'api/encryption/noop.dart'; -import 'api/hashing.dart'; -import 'api/hashing/argon2.dart'; -import 'api/kdf/hkdf.dart'; -import 'core/error.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; - -abstract class RustLibApiImplPlatform extends BaseApiImpl { - RustLibApiImplPlatform({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_CipherHandlePtr => wire - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle; - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_HasherHandlePtr => wire - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle; - - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_NoopEncryptionPtr => wire - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption; - - @protected - CipherHandle - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ); - - @protected - HasherHandle - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ); - - @protected - NoopEncryption - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ); - - @protected - CipherHandle - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ); - - @protected - HasherHandle - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ); - - @protected - NoopEncryption - dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ); - - @protected - CipherHandle - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - dynamic raw, - ); - - @protected - HasherHandle - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - dynamic raw, - ); - - @protected - NoopEncryption - dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - dynamic raw, - ); - - @protected - String dco_decode_String(dynamic raw); - - @protected - Argon2Preset dco_decode_argon_2_preset(dynamic raw); - - @protected - CryptoError dco_decode_crypto_error(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - List dco_decode_list_prim_u_8_loose(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); - - @protected - Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); - - @protected - int dco_decode_u_8(dynamic raw); - - @protected - void dco_decode_unit(dynamic raw); - - @protected - BigInt dco_decode_usize(dynamic raw); - - @protected - CipherHandle - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ); - - @protected - HasherHandle - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ); - - @protected - NoopEncryption - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ); - - @protected - CipherHandle - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ); - - @protected - HasherHandle - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ); - - @protected - NoopEncryption - sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ); - - @protected - CipherHandle - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - SseDeserializer deserializer, - ); - - @protected - HasherHandle - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - SseDeserializer deserializer, - ); - - @protected - NoopEncryption - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - Argon2Preset sse_decode_argon_2_preset(SseDeserializer deserializer); - - @protected - CryptoError sse_decode_crypto_error(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - int sse_decode_u_8(SseDeserializer deserializer); - - @protected - void sse_decode_unit(SseDeserializer deserializer); - - @protected - BigInt sse_decode_usize(SseDeserializer deserializer); - - @protected - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - CipherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - HasherHandle self, - SseSerializer serializer, - ); - - @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - NoopEncryption self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_argon_2_preset(Argon2Preset self, SseSerializer serializer); - - @protected - void sse_encode_crypto_error(CryptoError self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_list_prim_u_8_strict( - Uint8List? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_u_8(int self, SseSerializer serializer); - - @protected - void sse_encode_unit(void self, SseSerializer serializer); - - @protected - void sse_encode_usize(BigInt self, SseSerializer serializer); - - @protected - void sse_encode_bool(bool self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - RustLibWire.fromExternalLibrary(ExternalLibrary lib); - - void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - int ptr, - ) => wasmModule - .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - ptr, - ); - - void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - int ptr, - ) => wasmModule - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - ptr, - ); - - void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - int ptr, - ) => wasmModule - .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - ptr, - ); - - void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - int ptr, - ) => wasmModule - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - ptr, - ); - - void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - int ptr, - ) => wasmModule - .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr, - ); - - void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - int ptr, - ) => wasmModule - .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr, - ); -} - -@JS('wasm_bindgen') -external RustLibWasmModule get wasmModule; - -@JS() -@anonymous -extension type RustLibWasmModule._(JSObject _) implements JSObject { - external void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - int ptr, - ); - - external void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCipherHandle( - int ptr, - ); - - external void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - int ptr, - ); - - external void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerHasherHandle( - int ptr, - ); - - external void - rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - int ptr, - ); - - external void - rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - int ptr, - ); -} diff --git a/lib/src/streaming/streaming_service.dart b/lib/src/streaming/streaming_service.dart new file mode 100644 index 0000000..b92b2bc --- /dev/null +++ b/lib/src/streaming/streaming_service.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:m_security/src/rust/api/streaming.dart' as rust_streaming; +import 'package:m_security/src/rust/api/encryption.dart' as rust_encryption; +import 'package:m_security/src/rust/api/hashing.dart' as rust_hashing; + +/// Streaming file operations (encrypt, decrypt, hash). +/// +/// Processes large files in 64KB chunks to maintain constant RAM usage +/// regardless of file size. +class StreamingService { + StreamingService._(); + + /// Encrypt a file, writing the result to outputPath. + /// Returns a Stream of progress (0.0 to 1.0). + static Stream encryptFile({ + required String inputPath, + required String outputPath, + required rust_encryption.CipherHandle cipher, + }) { + return _guardedStream(() => rust_streaming.streamEncryptFile( + cipher: cipher, + inputPath: inputPath, + outputPath: outputPath, + )); + } + + /// Decrypt a streaming-encrypted file. + /// Returns a Stream of progress (0.0 to 1.0). + static Stream decryptFile({ + required String inputPath, + required String outputPath, + required rust_encryption.CipherHandle cipher, + }) { + return _guardedStream(() => rust_streaming.streamDecryptFile( + cipher: cipher, + inputPath: inputPath, + outputPath: outputPath, + )); + } + + /// Hash a file without loading it into memory. + /// Returns the digest bytes. + static Future hashFile({ + required String filePath, + required rust_hashing.HasherHandle hasher, + }) async { + await _guardedStream(() => rust_streaming.streamHashFile( + hasher: hasher, + filePath: filePath, + )).drain(); + return await rust_hashing.hasherFinalize(handle: hasher); + } + + // FRB stream functions use unawaited(handler.executeNormal(...)), + // so the Rust error is thrown as a zone error, not a stream error. + // The error arrives AFTER the progress stream closes, so we delay + // closing the controller to let the zone handler forward it first. + static Stream _guardedStream(Stream Function() factory) { + final controller = StreamController(); + runZonedGuarded(() { + factory().listen( + controller.add, + onError: controller.addError, + onDone: () { + // FRB delivers errors after the stream closes — schedule close + // in the event loop so pending microtasks (zone errors) run first. + Future(() { + if (!controller.isClosed) controller.close(); + }); + }, + ); + }, (error, stack) { + if (!controller.isClosed) { + controller.addError(error, stack); + controller.close(); + } + }); + return controller.stream; + } +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8558b91..9fbe158 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -72,6 +72,21 @@ dependencies = [ "backtrace", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android_log-sys" version = "0.3.2" @@ -190,6 +205,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "build-target" version = "0.4.0" @@ -366,6 +402,22 @@ dependencies = [ "regex", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -637,6 +689,12 @@ version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "lock_api" version = "0.4.14" @@ -898,6 +956,7 @@ dependencies = [ "aes-gcm", "argon2", "blake3", + "brotli", "chacha20poly1305", "flutter_rust_bridge", "hex", @@ -906,6 +965,8 @@ dependencies = [ "rand", "sha2", "sha3", + "subtle", + "tempfile", "thiserror", "zeroize", "zstd", @@ -917,6 +978,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -982,6 +1056,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1147,6 +1233,97 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ee9f2ab..1c1cdc1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,12 +35,16 @@ argon2 = "0.5" hkdf = "0.12" sha2 = "0.10" -# Compression +# Constant-time comparison +subtle = "2.6" + +# Compression zstd = { version = "0.13", optional = true } +brotli = { version = "7.0", optional = true } [features] -default = [] -compression = ["zstd"] +default = ["compression"] +compression = ["zstd", "brotli"] testing = [] # exposes noop cipher — never enable in production builds [profile.release] @@ -48,6 +52,7 @@ panic = "abort" [dev-dependencies] hex = "0.4" +tempfile = "3" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/rust/src/api/compression/mod.rs b/rust/src/api/compression/mod.rs new file mode 100644 index 0000000..aa097ec --- /dev/null +++ b/rust/src/api/compression/mod.rs @@ -0,0 +1,263 @@ +//! Compression API module — Zstd, Brotli, and MIME-aware skip. + +// Internal compression implementations live in core/compression/ +// to avoid FRB codegen scanning them. + +use crate::core::error::CryptoError; + +/// Which compression algorithm to use. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompressionAlgorithm { + Zstd, + Brotli, + None, +} + +impl CompressionAlgorithm { + /// Serialize to the byte stored in the stream header. + pub fn to_u8(self) -> u8 { + match self { + Self::None => 0x00, + Self::Zstd => 0x01, + Self::Brotli => 0x02, + } + } + + /// Deserialize from the byte stored in the stream header. + pub fn from_u8(byte: u8) -> Result { + match byte { + 0x00 => Ok(Self::None), + 0x01 => Ok(Self::Zstd), + 0x02 => Ok(Self::Brotli), + other => Err(CryptoError::InvalidParameter(format!( + "Unknown compression algorithm byte: {other:#04x}" + ))), + } + } +} + +/// Configuration for a compress operation. +#[derive(Debug, Clone)] +pub struct CompressionConfig { + pub algorithm: CompressionAlgorithm, + /// Compression level. Valid range depends on algorithm: + /// - Zstd: 1–22 (default 3) + /// - Brotli: 0–11 (default 4) + /// - None: ignored + pub level: Option, +} + +/// Compress a byte buffer according to `config`. +#[cfg(feature = "compression")] +pub fn compress(data: &[u8], config: &CompressionConfig) -> Result, CryptoError> { + use crate::core::compression::{brotli_impl, zstd_impl}; + match config.algorithm { + CompressionAlgorithm::Zstd => { + let level = config.level.unwrap_or(zstd_impl::DEFAULT_LEVEL); + zstd_impl::compress(data, level) + } + CompressionAlgorithm::Brotli => { + let level = config.level.unwrap_or(brotli_impl::DEFAULT_LEVEL as i32); + let level = u32::try_from(level).map_err(|_| { + CryptoError::InvalidParameter(format!( + "Brotli level must be {}–{}, got {level}", + brotli_impl::MIN_LEVEL, + brotli_impl::MAX_LEVEL + )) + })?; + brotli_impl::compress(data, level) + } + CompressionAlgorithm::None => Ok(data.to_vec()), + } +} + +/// Decompress a byte buffer produced by the given algorithm. +#[cfg(feature = "compression")] +pub fn decompress(data: &[u8], algorithm: CompressionAlgorithm) -> Result, CryptoError> { + use crate::core::compression::{brotli_impl, zstd_impl}; + match algorithm { + CompressionAlgorithm::Zstd => zstd_impl::decompress(data), + CompressionAlgorithm::Brotli => brotli_impl::decompress(data), + CompressionAlgorithm::None => Ok(data.to_vec()), + } +} + +/// Returns `true` when the file extension indicates already-compressed data. +pub fn should_skip_compression(file_path: &str) -> bool { + use std::path::Path; + + const SKIP: &[&str] = &[ + // images + "jpg", "jpeg", "png", "gif", "webp", + // video + "mp4", "mkv", "avi", "mov", "webm", + // audio + "mp3", "aac", "ogg", "flac", + // archives + "zip", "gz", "bz2", "xz", "zst", "br", "7z", "rar", + ]; + + Path::new(file_path) + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| SKIP.contains(&ext.to_ascii_lowercase().as_str())) + .unwrap_or(false) +} + +#[cfg(all(test, feature = "compression"))] +mod tests { + use super::*; + + // -- roundtrip -------------------------------------------------------- + + #[test] + fn test_zstd_roundtrip() { + let data = b"Hello, Zstd compression roundtrip!"; + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + }; + let compressed = compress(data, &config).expect("zstd compress"); + let decompressed = + decompress(&compressed, CompressionAlgorithm::Zstd).expect("zstd decompress"); + assert_eq!(decompressed, data); + } + + #[test] + fn test_brotli_roundtrip() { + let data = b"Hello, Brotli compression roundtrip!"; + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: None, + }; + let compressed = compress(data, &config).expect("brotli compress"); + let decompressed = + decompress(&compressed, CompressionAlgorithm::Brotli).expect("brotli decompress"); + assert_eq!(decompressed, data); + } + + // -- custom levels ---------------------------------------------------- + + #[test] + fn test_zstd_custom_level() { + let data = b"level test data for zstd"; + for level in [1, 19] { + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: Some(level), + }; + let compressed = compress(data, &config).expect("zstd compress"); + let decompressed = + decompress(&compressed, CompressionAlgorithm::Zstd).expect("zstd decompress"); + assert_eq!(decompressed, data); + } + } + + #[test] + fn test_brotli_custom_level() { + let data = b"level test data for brotli"; + for level in [0, 11] { + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: Some(level), + }; + let compressed = compress(data, &config).expect("brotli compress"); + let decompressed = + decompress(&compressed, CompressionAlgorithm::Brotli).expect("brotli decompress"); + assert_eq!(decompressed, data); + } + } + + // -- level validation ------------------------------------------------- + + #[test] + fn test_zstd_invalid_level() { + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: Some(0), + }; + assert!(compress(b"x", &config).is_err()); + + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: Some(23), + }; + assert!(compress(b"x", &config).is_err()); + } + + #[test] + fn test_brotli_invalid_level() { + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: Some(12), + }; + assert!(compress(b"x", &config).is_err()); + + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: Some(-1), + }; + assert!(compress(b"x", &config).is_err()); + } + + // -- None passthrough ------------------------------------------------- + + #[test] + fn test_none_passthrough() { + let data = b"should stay the same"; + let config = CompressionConfig { + algorithm: CompressionAlgorithm::None, + level: None, + }; + let result = compress(data, &config).expect("none compress"); + assert_eq!(result, data); + + let result = decompress(data, CompressionAlgorithm::None).expect("none decompress"); + assert_eq!(result, data); + } + + // -- MIME-aware skip -------------------------------------------------- + + #[test] + fn test_should_skip_compressed_extensions() { + assert!(should_skip_compression("photo.jpg")); + assert!(should_skip_compression("photo.JPEG")); + assert!(should_skip_compression("archive.zip")); + assert!(should_skip_compression("archive.ZIP")); + assert!(should_skip_compression("video.mp4")); + assert!(should_skip_compression("VIDEO.MP4")); + assert!(should_skip_compression("SONG.MP3")); + } + + #[test] + fn test_should_not_skip_uncompressed_extensions() { + assert!(!should_skip_compression("notes.txt")); + assert!(!should_skip_compression("data.json")); + assert!(!should_skip_compression("report.pdf")); + } + + // -- empty data ------------------------------------------------------- + + #[test] + fn test_empty_data() { + let empty: &[u8] = &[]; + + let config_zstd = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + }; + let compressed = compress(empty, &config_zstd).expect("zstd compress empty"); + let decompressed = + decompress(&compressed, CompressionAlgorithm::Zstd).expect("zstd decompress empty"); + assert!(decompressed.is_empty()); + + let config_brotli = CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: None, + }; + let compressed = compress(empty, &config_brotli).expect("brotli compress empty"); + let decompressed = decompress(&compressed, CompressionAlgorithm::Brotli) + .expect("brotli decompress empty"); + assert!(decompressed.is_empty()); + } +} diff --git a/rust/src/api/encryption/mod.rs b/rust/src/api/encryption/mod.rs index 96dd5ac..9d5ed96 100644 --- a/rust/src/api/encryption/mod.rs +++ b/rust/src/api/encryption/mod.rs @@ -18,6 +18,21 @@ impl CipherHandle { fn new(cipher: Box) -> Self { Self { inner: cipher } } + + /// Direct encrypt for internal use (streaming). Not FRB-visible. + pub(crate) fn encrypt_raw(&self, plaintext: &[u8], aad: &[u8]) -> Result, CryptoError> { + self.inner.encrypt(plaintext, aad) + } + + /// Direct decrypt for internal use (streaming). Not FRB-visible. + pub(crate) fn decrypt_raw(&self, ciphertext: &[u8], aad: &[u8]) -> Result, CryptoError> { + self.inner.decrypt(ciphertext, aad) + } + + /// Get the algorithm_id for internal use (streaming header). + pub(crate) fn algorithm_id(&self) -> &'static str { + self.inner.algorithm_id() + } } /// Create a noop encryption handle (for testing FRB opaque pattern). diff --git a/rust/src/api/encryption/noop.rs b/rust/src/api/encryption/noop.rs index d753200..0caed1a 100644 --- a/rust/src/api/encryption/noop.rs +++ b/rust/src/api/encryption/noop.rs @@ -7,7 +7,7 @@ use crate::core::traits::Encryption; /// A no-op cipher that returns data unchanged. /// Used to validate FRB opaque handle pattern works. -#[frb(opaque)] // must be opaque to maintain SecretBuffer guanrantees. +#[frb(ignore)] pub struct NoopEncryption {} impl Encryption for NoopEncryption { diff --git a/rust/src/api/evfs/format.rs b/rust/src/api/evfs/format.rs new file mode 100644 index 0000000..a3fa50d --- /dev/null +++ b/rust/src/api/evfs/format.rs @@ -0,0 +1,929 @@ +//! On-disk `.vault` format: header, segment index, free-region list, layout constants. + +use crate::api::compression::CompressionAlgorithm; +use crate::core::error::CryptoError; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +pub const VAULT_MAGIC: &[u8; 4] = b"MVLT"; +pub const VAULT_VERSION: u8 = 1; +pub const VAULT_HEADER_SIZE: usize = 32; + +/// Max segment name length in bytes (UTF-8). +pub const MAX_SEGMENT_NAME_LEN: usize = 255; + +/// Index is always serialized to this size (zero-padded). +/// 64KB supports approximately 200 segments + free regions. +pub const INDEX_PAD_SIZE: usize = 64 * 1024; + +/// Vault file layout offsets. +pub const PRIMARY_INDEX_OFFSET: u64 = VAULT_HEADER_SIZE as u64; +pub const DATA_REGION_OFFSET: u64 = PRIMARY_INDEX_OFFSET + INDEX_PAD_SIZE as u64; + +/// Shadow index offset depends on vault capacity. +pub fn shadow_index_offset(capacity: u64) -> Result { + DATA_REGION_OFFSET + .checked_add(capacity) + .ok_or_else(|| CryptoError::InvalidParameter("vault capacity overflows layout".into())) +} + +/// WAL region starts after the shadow index. +pub fn wal_region_offset(capacity: u64) -> Result { + shadow_index_offset(capacity)? + .checked_add(INDEX_PAD_SIZE as u64) + .ok_or_else(|| CryptoError::InvalidParameter("vault capacity overflows layout".into())) +} + +// --------------------------------------------------------------------------- +// VaultHeader +// --------------------------------------------------------------------------- + +/// On-disk vault header (32 bytes). +/// +/// Layout: `[MAGIC(4)] [VERSION(1)] [ALGORITHM(1)] [FLAGS(2)] [RESERVED(24)]` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VaultHeader { + pub version: u8, + /// AEAD algorithm ID (reuses `core::format::Algorithm` byte values). + pub algorithm: u8, + pub flags: u16, +} + +impl VaultHeader { + pub fn new(algorithm: u8) -> Self { + Self { + version: VAULT_VERSION, + algorithm, + flags: 0, + } + } + + pub fn to_bytes(&self) -> [u8; VAULT_HEADER_SIZE] { + let mut buf = [0u8; VAULT_HEADER_SIZE]; + buf[0..4].copy_from_slice(VAULT_MAGIC); + buf[4] = self.version; + buf[5] = self.algorithm; + buf[6..8].copy_from_slice(&self.flags.to_le_bytes()); + // bytes 8..32 reserved (zeros) + buf + } + + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < VAULT_HEADER_SIZE { + return Err(CryptoError::VaultCorrupted(format!( + "header too short: {} bytes, need {VAULT_HEADER_SIZE}", + data.len() + ))); + } + if &data[0..4] != VAULT_MAGIC { + return Err(CryptoError::VaultCorrupted( + "invalid magic bytes".to_string(), + )); + } + let version = data[4]; + if version != VAULT_VERSION { + return Err(CryptoError::VaultCorrupted(format!( + "unsupported vault version: {version}" + ))); + } + let algorithm = data[5]; + let flags = u16::from_le_bytes([data[6], data[7]]); + Ok(Self { + version, + algorithm, + flags, + }) + } +} + +// --------------------------------------------------------------------------- +// FreeRegion +// --------------------------------------------------------------------------- + +/// A free region in the data area available for reuse. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FreeRegion { + /// Byte offset relative to data region start. + pub offset: u64, + pub size: u64, +} + +// --------------------------------------------------------------------------- +// SegmentEntry +// --------------------------------------------------------------------------- + +/// Segment index entry — one per stored segment. +#[derive(Debug, Clone)] +pub struct SegmentEntry { + /// Segment name (1–255 bytes UTF-8). + pub name: String, + /// Byte offset in vault data region. + pub offset: u64, + /// Encrypted segment size (nonce + ciphertext + tag). + pub size: u64, + /// Write generation (increments on overwrite). + pub generation: u64, + /// BLAKE3 hash of original plaintext (pre-compression). + pub checksum: [u8; 32], + /// Algorithm used to compress this segment. + pub compression: CompressionAlgorithm, +} + +impl SegmentEntry { + pub fn new( + name: &str, + offset: u64, + size: u64, + generation: u64, + checksum: [u8; 32], + compression: CompressionAlgorithm, + ) -> Result { + if name.is_empty() || name.len() > MAX_SEGMENT_NAME_LEN { + return Err(CryptoError::InvalidParameter(format!( + "segment name must be 1\u{2013}{MAX_SEGMENT_NAME_LEN} bytes, got {}", + name.len() + ))); + } + Ok(Self { + name: name.to_string(), + offset, + size, + generation, + checksum, + compression, + }) + } +} + +// --------------------------------------------------------------------------- +// SegmentIndex +// --------------------------------------------------------------------------- + +/// Encrypted segment index — stored encrypted after the header. +/// +/// Tracks live segments, freed regions for space reclamation, and the +/// append cursor (`next_free_offset`) for when no free region fits. +#[derive(Debug, Clone)] +pub struct SegmentIndex { + pub entries: Vec, + /// Sorted by offset, adjacent regions merged. + pub free_regions: Vec, + /// Append cursor (relative to data region start). + pub next_free_offset: u64, + /// Total data region size in bytes. + pub capacity: u64, + /// Global generation counter. + pub next_generation: u64, +} + +// -- helpers for little-endian I/O ------------------------------------------ + +fn put_u16(buf: &mut Vec, v: u16) { + buf.extend_from_slice(&v.to_le_bytes()); +} + +fn put_u32(buf: &mut Vec, v: u32) { + buf.extend_from_slice(&v.to_le_bytes()); +} + +fn put_u64(buf: &mut Vec, v: u64) { + buf.extend_from_slice(&v.to_le_bytes()); +} + +fn read_u16(data: &[u8], off: &mut usize) -> Result { + let end = *off + 2; + if end > data.len() { + return Err(CryptoError::VaultCorrupted("index truncated (u16)".into())); + } + let v = u16::from_le_bytes([data[*off], data[*off + 1]]); + *off = end; + Ok(v) +} + +fn read_u32(data: &[u8], off: &mut usize) -> Result { + let end = *off + 4; + if end > data.len() { + return Err(CryptoError::VaultCorrupted("index truncated (u32)".into())); + } + let v = u32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); + *off = end; + Ok(v) +} + +fn read_u64(data: &[u8], off: &mut usize) -> Result { + let end = *off + 8; + if end > data.len() { + return Err(CryptoError::VaultCorrupted("index truncated (u64)".into())); + } + let v = u64::from_le_bytes([ + data[*off], + data[*off + 1], + data[*off + 2], + data[*off + 3], + data[*off + 4], + data[*off + 5], + data[*off + 6], + data[*off + 7], + ]); + *off = end; + Ok(v) +} + +fn read_bytes(data: &[u8], off: &mut usize, len: usize) -> Result, CryptoError> { + let end = *off + len; + if end > data.len() { + return Err(CryptoError::VaultCorrupted("index truncated (bytes)".into())); + } + let v = data[*off..end].to_vec(); + *off = end; + Ok(v) +} + +impl SegmentIndex { + /// Create an empty index for a new vault. + pub fn new(capacity: u64) -> Self { + Self { + entries: Vec::new(), + free_regions: Vec::new(), + next_free_offset: 0, + capacity, + next_generation: 0, + } + } + + /// Serialize index to bytes, padded to `INDEX_PAD_SIZE`. + /// + /// Wire format: + /// ```text + /// [entry_count: u32] + /// [free_region_count: u32] + /// [next_free_offset: u64] + /// [capacity: u64] + /// [next_generation: u64] + /// -- entries -- + /// per entry: + /// [name_len: u16] [name: UTF-8] [offset: u64] [size: u64] + /// [generation: u64] [checksum: 32B] [compression: u8] + /// -- free regions -- + /// per region: + /// [offset: u64] [size: u64] + /// -- zero padding to INDEX_PAD_SIZE -- + /// ``` + pub fn to_bytes(&self) -> Result, CryptoError> { + let mut buf = Vec::with_capacity(INDEX_PAD_SIZE); + + let entry_count = u32::try_from(self.entries.len()).map_err(|_| { + CryptoError::VaultCorrupted("too many segment entries".into()) + })?; + let free_count = u32::try_from(self.free_regions.len()).map_err(|_| { + CryptoError::VaultCorrupted("too many free regions".into()) + })?; + + put_u32(&mut buf, entry_count); + put_u32(&mut buf, free_count); + put_u64(&mut buf, self.next_free_offset); + put_u64(&mut buf, self.capacity); + put_u64(&mut buf, self.next_generation); + + for entry in &self.entries { + let name_bytes = entry.name.as_bytes(); + let name_len = u16::try_from(name_bytes.len()).map_err(|_| { + CryptoError::InvalidParameter("segment name too long for u16".into()) + })?; + put_u16(&mut buf, name_len); + buf.extend_from_slice(name_bytes); + put_u64(&mut buf, entry.offset); + put_u64(&mut buf, entry.size); + put_u64(&mut buf, entry.generation); + buf.extend_from_slice(&entry.checksum); + buf.push(entry.compression.to_u8()); + } + + for region in &self.free_regions { + put_u64(&mut buf, region.offset); + put_u64(&mut buf, region.size); + } + + if buf.len() > INDEX_PAD_SIZE { + return Err(CryptoError::VaultCorrupted(format!( + "index content ({} bytes) exceeds INDEX_PAD_SIZE ({INDEX_PAD_SIZE})", + buf.len() + ))); + } + + buf.resize(INDEX_PAD_SIZE, 0); + Ok(buf) + } + + /// Deserialize index from decrypted bytes. + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 32 { + return Err(CryptoError::VaultCorrupted("index too short".into())); + } + + let mut off = 0; + let entry_count = read_u32(data, &mut off)? as usize; + let free_count = read_u32(data, &mut off)? as usize; + + // Sanity-cap: the smallest possible entry is ~59 bytes (1-byte name), + // free region is 16 bytes. Reject clearly corrupted counts early. + let max_entries = INDEX_PAD_SIZE / 59; + let max_free = INDEX_PAD_SIZE / 16; + if entry_count > max_entries { + return Err(CryptoError::VaultCorrupted(format!( + "entry count {entry_count} exceeds maximum {max_entries}" + ))); + } + if free_count > max_free { + return Err(CryptoError::VaultCorrupted(format!( + "free region count {free_count} exceeds maximum {max_free}" + ))); + } + + let next_free_offset = read_u64(data, &mut off)?; + let capacity = read_u64(data, &mut off)?; + let next_generation = read_u64(data, &mut off)?; + + let mut entries = Vec::with_capacity(entry_count); + for _ in 0..entry_count { + let name_len = read_u16(data, &mut off)? as usize; + if name_len == 0 || name_len > MAX_SEGMENT_NAME_LEN { + return Err(CryptoError::VaultCorrupted(format!( + "invalid segment name length: {name_len}" + ))); + } + let name_bytes = read_bytes(data, &mut off, name_len)?; + let name = String::from_utf8(name_bytes).map_err(|_| { + CryptoError::VaultCorrupted("segment name is not valid UTF-8".into()) + })?; + let offset = read_u64(data, &mut off)?; + let size = read_u64(data, &mut off)?; + let generation = read_u64(data, &mut off)?; + let checksum_bytes = read_bytes(data, &mut off, 32)?; + let mut checksum = [0u8; 32]; + checksum.copy_from_slice(&checksum_bytes); + let comp_byte = read_bytes(data, &mut off, 1)?; + let compression = CompressionAlgorithm::from_u8(comp_byte[0])?; + entries.push(SegmentEntry { + name, + offset, + size, + generation, + checksum, + compression, + }); + } + + let mut free_regions = Vec::with_capacity(free_count); + for _ in 0..free_count { + let offset = read_u64(data, &mut off)?; + let size = read_u64(data, &mut off)?; + free_regions.push(FreeRegion { offset, size }); + } + + Ok(Self { + entries, + free_regions, + next_free_offset, + capacity, + next_generation, + }) + } + + // -- lookup / mutation -------------------------------------------------- + + /// Add a segment entry. Rejects duplicate names. + pub fn add(&mut self, entry: SegmentEntry) -> Result<(), CryptoError> { + if self.entries.iter().any(|e| e.name == entry.name) { + return Err(CryptoError::InvalidParameter(format!( + "duplicate segment name: {}", + entry.name + ))); + } + self.entries.push(entry); + Ok(()) + } + + pub fn find(&self, name: &str) -> Option<&SegmentEntry> { + self.entries.iter().find(|e| e.name == name) + } + + pub fn find_mut(&mut self, name: &str) -> Option<&mut SegmentEntry> { + self.entries.iter_mut().find(|e| e.name == name) + } + + pub fn remove(&mut self, name: &str) -> Option { + if let Some(pos) = self.entries.iter().position(|e| e.name == name) { + Some(self.entries.remove(pos)) + } else { + None + } + } + + pub fn names(&self) -> Vec<&str> { + self.entries.iter().map(|e| e.name.as_str()).collect() + } + + // -- allocation --------------------------------------------------------- + + /// Allocate space for `size` bytes. Returns the data-region offset. + /// + /// Strategy: best-fit search in `free_regions` first (smallest region + /// that fits), then fall back to appending at `next_free_offset`. + pub fn allocate(&mut self, size: u64) -> Result { + if size == 0 { + return Err(CryptoError::InvalidParameter( + "cannot allocate zero bytes".into(), + )); + } + + // Best-fit search in free list + let best = self + .free_regions + .iter() + .enumerate() + .filter(|(_, r)| r.size >= size) + .min_by_key(|(_, r)| r.size); + + if let Some((idx, _)) = best { + let region = self.free_regions.remove(idx); + let offset = region.offset; + let leftover = region.size - size; + if leftover > 0 { + self.free_regions.push(FreeRegion { + offset: offset + size, + size: leftover, + }); + self.free_regions.sort_by_key(|r| r.offset); + } + return Ok(offset); + } + + // Fall back to append (checked arithmetic to prevent overflow) + let end = self.next_free_offset.checked_add(size).ok_or( + CryptoError::VaultFull { + needed: size, + available: self.capacity.saturating_sub(self.next_free_offset), + }, + )?; + if end > self.capacity { + return Err(CryptoError::VaultFull { + needed: size, + available: self.capacity.saturating_sub(self.next_free_offset), + }); + } + let offset = self.next_free_offset; + self.next_free_offset = end; + Ok(offset) + } + + /// Return a region to the free list. Merges adjacent regions. + pub fn deallocate(&mut self, offset: u64, size: u64) { + self.free_regions.push(FreeRegion { offset, size }); + self.free_regions.sort_by_key(|r| r.offset); + self.merge_adjacent(); + } + + fn merge_adjacent(&mut self) { + let mut i = 0; + while i + 1 < self.free_regions.len() { + let end = self.free_regions[i].offset + self.free_regions[i].size; + if end == self.free_regions[i + 1].offset { + self.free_regions[i].size += self.free_regions[i + 1].size; + self.free_regions.remove(i + 1); + } else { + i += 1; + } + } + } + + // -- generation --------------------------------------------------------- + + pub fn next_gen(&mut self) -> u64 { + let gen = self.next_generation; + self.next_generation += 1; + gen + } + + // -- stats -------------------------------------------------------------- + + pub fn used_bytes(&self) -> u64 { + self.entries.iter().map(|e| e.size).sum() + } + + pub fn free_list_bytes(&self) -> u64 { + self.free_regions.iter().map(|r| r.size).sum() + } +} + +// =========================================================================== +// Tests +// =========================================================================== + +#[cfg(test)] +mod tests { + use super::*; + + fn dummy_checksum(val: u8) -> [u8; 32] { + [val; 32] + } + + // -- VaultHeader -------------------------------------------------------- + + #[test] + fn test_vault_header_roundtrip() { + let header = VaultHeader::new(0x01); + let bytes = header.to_bytes(); + let parsed = VaultHeader::from_bytes(&bytes).expect("parse"); + assert_eq!(header, parsed); + } + + #[test] + fn test_vault_header_invalid_magic() { + let mut bytes = VaultHeader::new(0x01).to_bytes(); + bytes[0] = b'X'; + assert!(VaultHeader::from_bytes(&bytes).is_err()); + } + + #[test] + fn test_vault_header_invalid_version() { + let mut bytes = VaultHeader::new(0x01).to_bytes(); + bytes[4] = 99; + assert!(VaultHeader::from_bytes(&bytes).is_err()); + } + + // -- SegmentEntry ------------------------------------------------------- + + #[test] + fn test_segment_entry_valid_name() { + // 1 byte + let e = SegmentEntry::new("a", 0, 100, 0, dummy_checksum(0), CompressionAlgorithm::None); + assert!(e.is_ok()); + + // 255 bytes + let long = "a".repeat(MAX_SEGMENT_NAME_LEN); + let e = SegmentEntry::new(&long, 0, 100, 0, dummy_checksum(0), CompressionAlgorithm::None); + assert!(e.is_ok()); + } + + #[test] + fn test_segment_entry_name_too_long() { + let long = "a".repeat(MAX_SEGMENT_NAME_LEN + 1); + let e = SegmentEntry::new(&long, 0, 100, 0, dummy_checksum(0), CompressionAlgorithm::None); + assert!(e.is_err()); + } + + #[test] + fn test_segment_entry_name_empty() { + let e = SegmentEntry::new("", 0, 100, 0, dummy_checksum(0), CompressionAlgorithm::None); + assert!(e.is_err()); + } + + // -- SegmentIndex serialization ----------------------------------------- + + fn make_test_index() -> SegmentIndex { + let mut idx = SegmentIndex::new(1024 * 1024); + idx.add( + SegmentEntry::new("hello.txt", 0, 4096, 1, dummy_checksum(0xAA), CompressionAlgorithm::Zstd) + .expect("entry"), + ).expect("add"); + idx.add( + SegmentEntry::new("photo.jpg", 4096, 8192, 2, dummy_checksum(0xBB), CompressionAlgorithm::None) + .expect("entry"), + ).expect("add"); + idx.free_regions.push(FreeRegion { offset: 12288, size: 2048 }); + idx.next_free_offset = 14336; + idx.next_generation = 3; + idx + } + + #[test] + fn test_segment_index_roundtrip() { + let idx = make_test_index(); + let bytes = idx.to_bytes().expect("serialize"); + let parsed = SegmentIndex::from_bytes(&bytes).expect("parse"); + + assert_eq!(parsed.entries.len(), 2); + assert_eq!(parsed.entries[0].name, "hello.txt"); + assert_eq!(parsed.entries[0].offset, 0); + assert_eq!(parsed.entries[0].size, 4096); + assert_eq!(parsed.entries[0].generation, 1); + assert_eq!(parsed.entries[0].checksum, dummy_checksum(0xAA)); + assert_eq!(parsed.entries[0].compression, CompressionAlgorithm::Zstd); + + assert_eq!(parsed.entries[1].name, "photo.jpg"); + assert_eq!(parsed.entries[1].compression, CompressionAlgorithm::None); + + assert_eq!(parsed.free_regions.len(), 1); + assert_eq!(parsed.free_regions[0].offset, 12288); + assert_eq!(parsed.free_regions[0].size, 2048); + + assert_eq!(parsed.next_free_offset, 14336); + assert_eq!(parsed.capacity, 1024 * 1024); + assert_eq!(parsed.next_generation, 3); + } + + #[test] + fn test_segment_index_roundtrip_with_generation() { + let mut idx = SegmentIndex::new(1024); + idx.next_generation = 42; + idx.entries.push( + SegmentEntry::new("gen", 0, 64, 41, dummy_checksum(0), CompressionAlgorithm::None) + .expect("entry"), + ); + let bytes = idx.to_bytes().expect("serialize"); + let parsed = SegmentIndex::from_bytes(&bytes).expect("parse"); + assert_eq!(parsed.next_generation, 42); + assert_eq!(parsed.entries[0].generation, 41); + } + + #[test] + fn test_segment_index_compression_field() { + let mut idx = SegmentIndex::new(1024 * 1024); + for (i, algo) in [CompressionAlgorithm::Zstd, CompressionAlgorithm::Brotli, CompressionAlgorithm::None] + .iter() + .enumerate() + { + idx.entries.push( + SegmentEntry::new( + &format!("seg{i}"), + (i as u64) * 1024, + 1024, + 0, + dummy_checksum(i as u8), + *algo, + ) + .expect("entry"), + ); + } + let bytes = idx.to_bytes().expect("serialize"); + let parsed = SegmentIndex::from_bytes(&bytes).expect("parse"); + assert_eq!(parsed.entries[0].compression, CompressionAlgorithm::Zstd); + assert_eq!(parsed.entries[1].compression, CompressionAlgorithm::Brotli); + assert_eq!(parsed.entries[2].compression, CompressionAlgorithm::None); + } + + #[test] + fn test_segment_index_free_regions_roundtrip() { + let mut idx = SegmentIndex::new(1024 * 1024); + idx.free_regions.push(FreeRegion { offset: 0, size: 100 }); + idx.free_regions.push(FreeRegion { offset: 500, size: 200 }); + idx.free_regions.push(FreeRegion { offset: 1000, size: 300 }); + let bytes = idx.to_bytes().expect("serialize"); + let parsed = SegmentIndex::from_bytes(&bytes).expect("parse"); + assert_eq!(parsed.free_regions.len(), 3); + assert_eq!(parsed.free_regions[0], FreeRegion { offset: 0, size: 100 }); + assert_eq!(parsed.free_regions[1], FreeRegion { offset: 500, size: 200 }); + assert_eq!(parsed.free_regions[2], FreeRegion { offset: 1000, size: 300 }); + } + + #[test] + fn test_segment_index_padded_size() { + // Empty index + let idx = SegmentIndex::new(1024); + let bytes = idx.to_bytes().expect("serialize"); + assert_eq!(bytes.len(), INDEX_PAD_SIZE); + + // Index with entries + free regions + let idx = make_test_index(); + let bytes = idx.to_bytes().expect("serialize"); + assert_eq!(bytes.len(), INDEX_PAD_SIZE); + } + + #[test] + fn test_segment_index_overflow() { + let mut idx = SegmentIndex::new(u64::MAX); + // Each entry with a 255-byte name uses 2 + 255 + 8 + 8 + 8 + 32 + 1 = 314 bytes. + // Index header is 32 bytes. (65536 - 32) / 314 ≈ 208 entries max. + for i in 0..210 { + let name = format!("{:0>255}", i); + idx.entries.push( + SegmentEntry::new(&name, 0, 64, 0, dummy_checksum(0), CompressionAlgorithm::None) + .expect("entry"), + ); + } + assert!(idx.to_bytes().is_err()); + } + + // -- SegmentIndex lookup / mutation -------------------------------------- + + #[test] + fn test_segment_index_find() { + let idx = make_test_index(); + let found = idx.find("hello.txt"); + assert!(found.is_some()); + assert_eq!(found.expect("find").offset, 0); + + assert!(idx.find("nonexistent").is_none()); + } + + #[test] + fn test_segment_index_remove() { + let mut idx = make_test_index(); + let removed = idx.remove("hello.txt"); + assert!(removed.is_some()); + assert_eq!(removed.expect("remove").name, "hello.txt"); + assert!(idx.find("hello.txt").is_none()); + assert_eq!(idx.entries.len(), 1); + } + + // -- Allocation --------------------------------------------------------- + + #[test] + fn test_allocate_appends_when_no_free_regions() { + let mut idx = SegmentIndex::new(1024); + let off1 = idx.allocate(100).expect("alloc"); + assert_eq!(off1, 0); + assert_eq!(idx.next_free_offset, 100); + + let off2 = idx.allocate(200).expect("alloc"); + assert_eq!(off2, 100); + assert_eq!(idx.next_free_offset, 300); + } + + #[test] + fn test_allocate_reuses_free_region_exact_fit() { + let mut idx = SegmentIndex::new(1024); + idx.free_regions.push(FreeRegion { offset: 0, size: 100 }); + + let off = idx.allocate(100).expect("alloc"); + assert_eq!(off, 0); + assert!(idx.free_regions.is_empty()); + } + + #[test] + fn test_allocate_reuses_free_region_with_split() { + let mut idx = SegmentIndex::new(1024); + idx.free_regions.push(FreeRegion { offset: 0, size: 300 }); + + let off = idx.allocate(100).expect("alloc"); + assert_eq!(off, 0); + assert_eq!(idx.free_regions.len(), 1); + assert_eq!(idx.free_regions[0].offset, 100); + assert_eq!(idx.free_regions[0].size, 200); + } + + #[test] + fn test_allocate_best_fit() { + let mut idx = SegmentIndex::new(1024); + idx.free_regions.push(FreeRegion { offset: 0, size: 500 }); + idx.free_regions.push(FreeRegion { offset: 600, size: 150 }); + idx.free_regions.push(FreeRegion { offset: 800, size: 200 }); + + // Needs 150 — should pick region at 600 (exact fit 150) + let off = idx.allocate(150).expect("alloc"); + assert_eq!(off, 600); + // The 150-byte region was consumed exactly + assert_eq!(idx.free_regions.len(), 2); + } + + #[test] + fn test_allocate_falls_back_to_append() { + let mut idx = SegmentIndex::new(1024); + idx.free_regions.push(FreeRegion { offset: 0, size: 50 }); + idx.next_free_offset = 100; + + // Needs 80 — free region only has 50, so append at 100 + let off = idx.allocate(80).expect("alloc"); + assert_eq!(off, 100); + assert_eq!(idx.next_free_offset, 180); + // Free region untouched + assert_eq!(idx.free_regions.len(), 1); + } + + #[test] + fn test_allocate_vault_full() { + let mut idx = SegmentIndex::new(100); + idx.next_free_offset = 80; + + let err = idx.allocate(50).unwrap_err(); + match err { + CryptoError::VaultFull { needed, available } => { + assert_eq!(needed, 50); + assert_eq!(available, 20); + } + other => panic!("expected VaultFull, got {other:?}"), + } + } + + // -- Deallocation ------------------------------------------------------- + + #[test] + fn test_deallocate_adds_to_free_list() { + let mut idx = SegmentIndex::new(1024); + idx.deallocate(100, 50); + assert_eq!(idx.free_regions.len(), 1); + assert_eq!(idx.free_regions[0], FreeRegion { offset: 100, size: 50 }); + } + + #[test] + fn test_deallocate_merges_adjacent() { + let mut idx = SegmentIndex::new(1024); + idx.deallocate(100, 50); + idx.deallocate(150, 50); + assert_eq!(idx.free_regions.len(), 1); + assert_eq!(idx.free_regions[0], FreeRegion { offset: 100, size: 100 }); + } + + #[test] + fn test_deallocate_no_merge_non_adjacent() { + let mut idx = SegmentIndex::new(1024); + idx.deallocate(100, 50); + idx.deallocate(200, 50); + assert_eq!(idx.free_regions.len(), 2); + } + + #[test] + fn test_deallocate_triple_merge() { + let mut idx = SegmentIndex::new(1024); + // Free A, C first (gap at B), then free B to trigger triple merge + idx.deallocate(0, 100); + idx.deallocate(200, 100); + assert_eq!(idx.free_regions.len(), 2); + + // Free the middle gap + idx.deallocate(100, 100); + assert_eq!(idx.free_regions.len(), 1); + assert_eq!(idx.free_regions[0], FreeRegion { offset: 0, size: 300 }); + } + + #[test] + fn test_allocate_after_deallocate() { + let mut idx = SegmentIndex::new(1024); + // Allocate then free a region + let off = idx.allocate(200).expect("alloc"); + assert_eq!(off, 0); + idx.deallocate(0, 200); + + // Next allocation should reuse the freed space + let off = idx.allocate(200).expect("realloc"); + assert_eq!(off, 0); + assert!(idx.free_regions.is_empty()); + } + + // -- Generation counter ------------------------------------------------- + + #[test] + fn test_generation_counter_increments() { + let mut idx = SegmentIndex::new(1024); + assert_eq!(idx.next_gen(), 0); + assert_eq!(idx.next_gen(), 1); + assert_eq!(idx.next_gen(), 2); + } + + // -- Layout constants --------------------------------------------------- + + #[test] + fn test_layout_offsets() { + // Primary index immediately after header + assert_eq!(PRIMARY_INDEX_OFFSET, VAULT_HEADER_SIZE as u64); + + // Data region after primary index + assert_eq!(DATA_REGION_OFFSET, PRIMARY_INDEX_OFFSET + INDEX_PAD_SIZE as u64); + + let cap = 10 * 1024 * 1024; // 10MB + let shadow = shadow_index_offset(cap).expect("shadow"); + let wal = wal_region_offset(cap).expect("wal"); + + // Shadow index after data region + assert_eq!(shadow, DATA_REGION_OFFSET + cap); + + // WAL after shadow index + assert_eq!(wal, shadow + INDEX_PAD_SIZE as u64); + + // No overlapping regions + assert!(PRIMARY_INDEX_OFFSET < DATA_REGION_OFFSET); + assert!(DATA_REGION_OFFSET < shadow); + assert!(shadow < wal); + } + + #[test] + fn test_layout_overflow() { + assert!(shadow_index_offset(u64::MAX).is_err()); + assert!(wal_region_offset(u64::MAX).is_err()); + } + + // -- Zero-size allocation ----------------------------------------------- + + #[test] + fn test_allocate_zero_rejected() { + let mut idx = SegmentIndex::new(1024); + assert!(idx.allocate(0).is_err()); + } + + // -- Duplicate names ---------------------------------------------------- + + #[test] + fn test_add_duplicate_name_rejected() { + let mut idx = SegmentIndex::new(1024); + let e1 = SegmentEntry::new("dup", 0, 64, 0, dummy_checksum(0), CompressionAlgorithm::None) + .expect("entry"); + let e2 = SegmentEntry::new("dup", 64, 64, 1, dummy_checksum(1), CompressionAlgorithm::None) + .expect("entry"); + idx.add(e1).expect("first add"); + assert!(idx.add(e2).is_err()); + assert_eq!(idx.entries.len(), 1); + } +} diff --git a/rust/src/api/evfs/mod.rs b/rust/src/api/evfs/mod.rs new file mode 100644 index 0000000..ea1f8a0 --- /dev/null +++ b/rust/src/api/evfs/mod.rs @@ -0,0 +1,5 @@ +//! Encrypted Virtual File System — .vault container format and operations. + +pub mod format; +pub mod segment; +pub mod wal; diff --git a/rust/src/api/evfs/segment.rs b/rust/src/api/evfs/segment.rs new file mode 100644 index 0000000..7c10aaa --- /dev/null +++ b/rust/src/api/evfs/segment.rs @@ -0,0 +1,873 @@ +//! Per-segment encryption, integrity, and secure deletion. + +use hkdf::Hkdf; +use sha2::Sha256; +use subtle::ConstantTimeEq; +use zeroize::Zeroize; + +use crate::core::error::CryptoError; +use crate::core::format::Algorithm; +use crate::core::secret::SecretBuffer; + +#[cfg(feature = "compression")] +use crate::api::compression::{self, CompressionAlgorithm, CompressionConfig}; + +use std::io::{Seek, SeekFrom, Write}; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const CIPHER_KEY_INFO: &[u8] = b"msec-vault-cipher-key"; +const NONCE_KEY_INFO: &[u8] = b"msec-vault-nonce-key"; +const INDEX_KEY_INFO: &[u8] = b"msec-vault-index-key"; +const INDEX_NONCE_INFO: &[u8] = b"msec-vault-index-nonce"; + +const SUB_KEY_LEN: usize = 32; +const NONCE_LEN: usize = 12; +const TAG_LEN: usize = 16; + +/// Chunk size for streaming pre-allocation and secure erase (64KB). +const IO_CHUNK_SIZE: usize = 64 * 1024; + +// --------------------------------------------------------------------------- +// VaultKeys +// --------------------------------------------------------------------------- + +/// Derived sub-keys from the master key. All fields use SecretBuffer (ZeroizeOnDrop). +pub struct VaultKeys { + pub cipher_key: SecretBuffer, + pub nonce_key: SecretBuffer, + pub index_key: SecretBuffer, +} + +/// Derive three domain-separated sub-keys from a master key via HKDF-SHA256. +pub fn derive_vault_keys(master_key: &[u8]) -> Result { + if master_key.is_empty() { + return Err(CryptoError::InvalidKeyLength { + expected: 32, + actual: 0, + }); + } + let hk = Hkdf::::new(None, master_key); + + let mut cipher_key = SecretBuffer::from_size(SUB_KEY_LEN); + hk.expand(CIPHER_KEY_INFO, cipher_key.as_bytes_mut()) + .map_err(|_| CryptoError::KdfFailed("HKDF expand failed for cipher_key".into()))?; + + let mut nonce_key = SecretBuffer::from_size(SUB_KEY_LEN); + hk.expand(NONCE_KEY_INFO, nonce_key.as_bytes_mut()) + .map_err(|_| CryptoError::KdfFailed("HKDF expand failed for nonce_key".into()))?; + + let mut index_key = SecretBuffer::from_size(SUB_KEY_LEN); + hk.expand(INDEX_KEY_INFO, index_key.as_bytes_mut()) + .map_err(|_| CryptoError::KdfFailed("HKDF expand failed for index_key".into()))?; + + Ok(VaultKeys { + cipher_key, + nonce_key, + index_key, + }) +} + +// --------------------------------------------------------------------------- +// Nonce derivation +// --------------------------------------------------------------------------- + +/// Derive a deterministic nonce for a segment. +/// +/// `info = segment_index(LE) || generation(LE)` +/// +/// Including the generation counter ensures overwriting a segment at +/// the same index always produces a different nonce. +pub fn derive_segment_nonce( + nonce_key: &[u8], + segment_index: u64, + generation: u64, + nonce_len: usize, +) -> Result, CryptoError> { + let hk = Hkdf::::from_prk(nonce_key) + .map_err(|_| CryptoError::KdfFailed("nonce_key too short for HKDF-PRK".into()))?; + + let mut info = [0u8; 16]; + info[..8].copy_from_slice(&segment_index.to_le_bytes()); + info[8..].copy_from_slice(&generation.to_le_bytes()); + + let mut nonce = vec![0u8; nonce_len]; + hk.expand(&info, &mut nonce) + .map_err(|_| CryptoError::KdfFailed("HKDF expand failed for nonce".into()))?; + Ok(nonce) +} + +// --------------------------------------------------------------------------- +// AEAD helpers (algorithm dispatch) +// --------------------------------------------------------------------------- + +fn aead_encrypt( + key: &[u8], + nonce: &[u8], + plaintext: &[u8], + aad: &[u8], + algorithm: Algorithm, +) -> Result, CryptoError> { + match algorithm { + Algorithm::AesGcm => aead_encrypt_aes_gcm(key, nonce, plaintext, aad), + Algorithm::ChaCha20Poly1305 => aead_encrypt_chacha(key, nonce, plaintext, aad), + Algorithm::XChaCha20Poly1305 => Err(CryptoError::InvalidParameter( + "XChaCha20-Poly1305 not yet supported for EVFS".into(), + )), + } +} + +fn aead_decrypt( + key: &[u8], + nonce: &[u8], + ciphertext: &[u8], + aad: &[u8], + algorithm: Algorithm, +) -> Result, CryptoError> { + match algorithm { + Algorithm::AesGcm => aead_decrypt_aes_gcm(key, nonce, ciphertext, aad), + Algorithm::ChaCha20Poly1305 => aead_decrypt_chacha(key, nonce, ciphertext, aad), + Algorithm::XChaCha20Poly1305 => Err(CryptoError::InvalidParameter( + "XChaCha20-Poly1305 not yet supported for EVFS".into(), + )), + } +} + +fn aead_encrypt_aes_gcm( + key: &[u8], + nonce: &[u8], + plaintext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + use aes_gcm::aead::{Aead, KeyInit, Payload}; + use aes_gcm::Aes256Gcm; + + let cipher = Aes256Gcm::new_from_slice(key) + .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; + let nonce = aes_gcm::Nonce::from_slice(nonce); + let payload = Payload { msg: plaintext, aad }; + cipher + .encrypt(nonce, payload) + .map_err(|e| CryptoError::EncryptionFailed(e.to_string())) +} + +fn aead_decrypt_aes_gcm( + key: &[u8], + nonce: &[u8], + ciphertext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + use aes_gcm::aead::{Aead, KeyInit, Payload}; + use aes_gcm::Aes256Gcm; + + let cipher = + Aes256Gcm::new_from_slice(key).map_err(|_| CryptoError::DecryptionFailed)?; + let nonce = aes_gcm::Nonce::from_slice(nonce); + let payload = Payload { + msg: ciphertext, + aad, + }; + cipher + .decrypt(nonce, payload) + .map_err(|_| CryptoError::AuthenticationFailed) +} + +fn aead_encrypt_chacha( + key: &[u8], + nonce: &[u8], + plaintext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + use chacha20poly1305::aead::{Aead, KeyInit, Payload}; + use chacha20poly1305::ChaCha20Poly1305; + + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; + let nonce = chacha20poly1305::Nonce::from_slice(nonce); + let payload = Payload { msg: plaintext, aad }; + cipher + .encrypt(nonce, payload) + .map_err(|e| CryptoError::EncryptionFailed(e.to_string())) +} + +fn aead_decrypt_chacha( + key: &[u8], + nonce: &[u8], + ciphertext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + use chacha20poly1305::aead::{Aead, KeyInit, Payload}; + use chacha20poly1305::ChaCha20Poly1305; + + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|_| CryptoError::DecryptionFailed)?; + let nonce = chacha20poly1305::Nonce::from_slice(nonce); + let payload = Payload { + msg: ciphertext, + aad, + }; + cipher + .decrypt(nonce, payload) + .map_err(|_| CryptoError::AuthenticationFailed) +} + +// --------------------------------------------------------------------------- +// Segment encrypt / decrypt +// --------------------------------------------------------------------------- + +/// Parameters for segment encrypt/decrypt operations. +pub struct SegmentCryptoParams<'a> { + pub cipher_key: &'a [u8], + pub nonce_key: &'a [u8], + pub algorithm: Algorithm, + pub segment_index: u64, + pub generation: u64, +} + +/// Compress-then-encrypt a segment's plaintext data. +/// +/// Returns `(nonce || ciphertext || tag, effective_compression_algorithm)`. +/// +/// BLAKE3 checksum should be computed by the caller on the original plaintext +/// **before** calling this function. +#[cfg(feature = "compression")] +pub fn encrypt_segment( + params: &SegmentCryptoParams<'_>, + plaintext: &[u8], + segment_name: &str, + compression: &CompressionConfig, +) -> Result<(Vec, CompressionAlgorithm), CryptoError> { + // MIME-aware skip + let effective_algo = if compression.algorithm != CompressionAlgorithm::None + && compression::should_skip_compression(segment_name) + { + CompressionAlgorithm::None + } else { + compression.algorithm + }; + + // Compress + let mut data = if effective_algo != CompressionAlgorithm::None { + compression::compress( + plaintext, + &CompressionConfig { + algorithm: effective_algo, + level: compression.level, + }, + )? + } else { + plaintext.to_vec() + }; + + // Derive nonce and encrypt + let nonce = derive_segment_nonce( + params.nonce_key, + params.segment_index, + params.generation, + NONCE_LEN, + )?; + let result = aead_encrypt(params.cipher_key, &nonce, &data, &[], params.algorithm); + data.zeroize(); + let ct_tag = result?; + + // Wire format: nonce || ciphertext || tag + let mut output = Vec::with_capacity(NONCE_LEN + ct_tag.len()); + output.extend_from_slice(&nonce); + output.extend_from_slice(&ct_tag); + + Ok((output, effective_algo)) +} + +/// Decrypt-then-decompress a segment's encrypted data. +/// +/// The `compression` argument comes from `SegmentEntry.compression`. +#[cfg(feature = "compression")] +pub fn decrypt_segment( + params: &SegmentCryptoParams<'_>, + encrypted: &[u8], + compression: CompressionAlgorithm, +) -> Result, CryptoError> { + if encrypted.len() < NONCE_LEN + TAG_LEN { + return Err(CryptoError::AuthenticationFailed); + } + + let (stored_nonce, ct_tag) = encrypted.split_at(NONCE_LEN); + + // Derive expected nonce and verify it matches + let expected_nonce = derive_segment_nonce( + params.nonce_key, + params.segment_index, + params.generation, + NONCE_LEN, + )?; + if stored_nonce.ct_ne(&expected_nonce).into() { + return Err(CryptoError::AuthenticationFailed); + } + + let decrypted = aead_decrypt(params.cipher_key, stored_nonce, ct_tag, &[], params.algorithm)?; + + // Decompress if needed + if compression != CompressionAlgorithm::None { + compression::decompress(&decrypted, compression) + } else { + Ok(decrypted) + } +} + +// --------------------------------------------------------------------------- +// Index encrypt / decrypt +// --------------------------------------------------------------------------- + +/// Encrypt the segment index using the index sub-key. +/// +/// `generation` is the `SegmentIndex.next_generation` value at the time of +/// writing. It is included in nonce derivation so that every index write +/// produces a unique nonce (preventing nonce reuse on repeated saves). +pub fn encrypt_index( + index_key: &[u8], + algorithm: Algorithm, + generation: u64, + plaintext: &[u8], +) -> Result, CryptoError> { + let nonce = derive_index_nonce(index_key, generation)?; + let ct_tag = aead_encrypt(index_key, &nonce, plaintext, &[], algorithm)?; + + let mut output = Vec::with_capacity(NONCE_LEN + ct_tag.len()); + output.extend_from_slice(&nonce); + output.extend_from_slice(&ct_tag); + Ok(output) +} + +/// Decrypt the segment index using the index sub-key. +/// +/// `generation` must match the value used during encryption. +pub fn decrypt_index( + index_key: &[u8], + algorithm: Algorithm, + generation: u64, + encrypted: &[u8], +) -> Result, CryptoError> { + if encrypted.len() < NONCE_LEN + TAG_LEN { + return Err(CryptoError::AuthenticationFailed); + } + let (stored_nonce, ct_tag) = encrypted.split_at(NONCE_LEN); + + // Verify nonce matches the expected derivation + let expected_nonce = derive_index_nonce(index_key, generation)?; + if stored_nonce.ct_ne(&expected_nonce).into() { + return Err(CryptoError::AuthenticationFailed); + } + + aead_decrypt(index_key, stored_nonce, ct_tag, &[], algorithm) +} + +fn derive_index_nonce(index_key: &[u8], generation: u64) -> Result, CryptoError> { + let hk = Hkdf::::from_prk(index_key) + .map_err(|_| CryptoError::KdfFailed("index_key too short for HKDF-PRK".into()))?; + // info = fixed domain || generation(LE) + let mut info = Vec::with_capacity(INDEX_NONCE_INFO.len() + 8); + info.extend_from_slice(INDEX_NONCE_INFO); + info.extend_from_slice(&generation.to_le_bytes()); + let mut nonce = vec![0u8; NONCE_LEN]; + hk.expand(&info, &mut nonce) + .map_err(|_| CryptoError::KdfFailed("HKDF expand failed for index nonce".into()))?; + Ok(nonce) +} + +// --------------------------------------------------------------------------- +// BLAKE3 checksums +// --------------------------------------------------------------------------- + +/// Compute BLAKE3 checksum of plaintext data (pre-compression). +pub fn compute_checksum(data: &[u8]) -> [u8; 32] { + blake3::hash(data).into() +} + +/// Verify BLAKE3 checksum using constant-time comparison. +pub fn verify_checksum(data: &[u8], expected: &[u8; 32]) -> bool { + let actual = compute_checksum(data); + actual.ct_eq(expected).into() +} + +// --------------------------------------------------------------------------- +// Pre-allocation and secure erase +// --------------------------------------------------------------------------- + +/// Pre-allocate a vault file filled with CSPRNG random data. +/// +/// Writes in 64KB chunks to keep memory constant for large vaults. +pub fn preallocate_vault( + file: &mut std::fs::File, + total_size: u64, +) -> Result<(), CryptoError> { + use rand::{rngs::OsRng, RngCore}; + + let mut remaining = total_size; + let mut buf = vec![0u8; IO_CHUNK_SIZE]; + + while remaining > 0 { + let chunk_len = std::cmp::min(remaining, IO_CHUNK_SIZE as u64) as usize; + OsRng.fill_bytes(&mut buf[..chunk_len]); + file.write_all(&buf[..chunk_len])?; + remaining -= chunk_len as u64; + } + file.sync_all()?; + Ok(()) +} + +/// Securely erase a region of the vault file by overwriting with CSPRNG bytes. +/// +/// Writes random data in 64KB chunks, then fsyncs. +pub fn secure_erase_region( + file: &mut std::fs::File, + offset: u64, + size: u64, +) -> Result<(), CryptoError> { + use rand::{rngs::OsRng, RngCore}; + + file.seek(SeekFrom::Start(offset))?; + + let mut remaining = size; + let mut buf = vec![0u8; IO_CHUNK_SIZE]; + + while remaining > 0 { + let chunk_len = std::cmp::min(remaining, IO_CHUNK_SIZE as u64) as usize; + OsRng.fill_bytes(&mut buf[..chunk_len]); + file.write_all(&buf[..chunk_len])?; + remaining -= chunk_len as u64; + } + file.sync_all()?; + Ok(()) +} + +// =========================================================================== +// Tests +// =========================================================================== + +#[cfg(test)] +mod tests { + use super::*; + + fn test_master_key() -> Vec { + vec![0xAA; 32] + } + + fn other_master_key() -> Vec { + vec![0xBB; 32] + } + + // -- Key derivation ----------------------------------------------------- + + #[test] + fn test_derive_vault_keys_empty_master_rejected() { + assert!(derive_vault_keys(&[]).is_err()); + } + + #[test] + fn test_derive_vault_keys_deterministic() { + let k1 = derive_vault_keys(&test_master_key()).expect("derive"); + let k2 = derive_vault_keys(&test_master_key()).expect("derive"); + assert_eq!(k1.cipher_key.as_bytes(), k2.cipher_key.as_bytes()); + assert_eq!(k1.nonce_key.as_bytes(), k2.nonce_key.as_bytes()); + assert_eq!(k1.index_key.as_bytes(), k2.index_key.as_bytes()); + } + + #[test] + fn test_derive_vault_keys_domain_separation() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + assert_ne!(keys.cipher_key.as_bytes(), keys.nonce_key.as_bytes()); + assert_ne!(keys.cipher_key.as_bytes(), keys.index_key.as_bytes()); + assert_ne!(keys.nonce_key.as_bytes(), keys.index_key.as_bytes()); + } + + #[test] + fn test_derive_vault_keys_different_master() { + let k1 = derive_vault_keys(&test_master_key()).expect("derive"); + let k2 = derive_vault_keys(&other_master_key()).expect("derive"); + assert_ne!(k1.cipher_key.as_bytes(), k2.cipher_key.as_bytes()); + assert_ne!(k1.nonce_key.as_bytes(), k2.nonce_key.as_bytes()); + assert_ne!(k1.index_key.as_bytes(), k2.index_key.as_bytes()); + } + + // -- Nonce derivation --------------------------------------------------- + + #[test] + fn test_nonce_deterministic() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let n1 = derive_segment_nonce(keys.nonce_key.as_bytes(), 0, 0, NONCE_LEN).expect("nonce"); + let n2 = derive_segment_nonce(keys.nonce_key.as_bytes(), 0, 0, NONCE_LEN).expect("nonce"); + assert_eq!(n1, n2); + } + + #[test] + fn test_nonce_unique_by_index() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let n0 = derive_segment_nonce(keys.nonce_key.as_bytes(), 0, 0, NONCE_LEN).expect("nonce"); + let n1 = derive_segment_nonce(keys.nonce_key.as_bytes(), 1, 0, NONCE_LEN).expect("nonce"); + assert_ne!(n0, n1); + } + + #[test] + fn test_nonce_unique_by_generation() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let n0 = derive_segment_nonce(keys.nonce_key.as_bytes(), 0, 0, NONCE_LEN).expect("nonce"); + let n1 = derive_segment_nonce(keys.nonce_key.as_bytes(), 0, 1, NONCE_LEN).expect("nonce"); + assert_ne!(n0, n1); + } + + #[test] + fn test_nonce_length_12() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let nonce = + derive_segment_nonce(keys.nonce_key.as_bytes(), 0, 0, NONCE_LEN).expect("nonce"); + assert_eq!(nonce.len(), 12); + } + + // -- Segment encrypt/decrypt (no compression) --------------------------- + + #[cfg(feature = "compression")] + fn no_compression_config() -> CompressionConfig { + CompressionConfig { + algorithm: CompressionAlgorithm::None, + level: None, + } + } + + #[cfg(feature = "compression")] + fn params<'a>( + keys: &'a VaultKeys, + algorithm: Algorithm, + segment_index: u64, + generation: u64, + ) -> SegmentCryptoParams<'a> { + SegmentCryptoParams { + cipher_key: keys.cipher_key.as_bytes(), + nonce_key: keys.nonce_key.as_bytes(), + algorithm, + segment_index, + generation, + } + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_encrypt_decrypt_roundtrip() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"hello vault segment"; + let p = params(&keys, Algorithm::AesGcm, 0, 0); + + let (encrypted, effective) = + encrypt_segment(&p, plaintext, "test.txt", &no_compression_config()) + .expect("encrypt"); + assert_eq!(effective, CompressionAlgorithm::None); + + let decrypted = + decrypt_segment(&p, &encrypted, CompressionAlgorithm::None).expect("decrypt"); + assert_eq!(decrypted, plaintext); + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_wrong_generation_fails() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let p = params(&keys, Algorithm::AesGcm, 0, 0); + + let (encrypted, _) = + encrypt_segment(&p, b"data", "test.txt", &no_compression_config()).expect("encrypt"); + + // Decrypt with wrong generation + let wrong_p = params(&keys, Algorithm::AesGcm, 0, 1); + let result = decrypt_segment(&wrong_p, &encrypted, CompressionAlgorithm::None); + assert!(result.is_err()); + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_wrong_key_fails() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let wrong_keys = derive_vault_keys(&other_master_key()).expect("derive"); + let p = params(&keys, Algorithm::AesGcm, 0, 0); + + let (encrypted, _) = + encrypt_segment(&p, b"secret", "test.txt", &no_compression_config()).expect("encrypt"); + + let wrong_p = params(&wrong_keys, Algorithm::AesGcm, 0, 0); + let result = decrypt_segment(&wrong_p, &encrypted, CompressionAlgorithm::None); + assert!(result.is_err()); + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_tampered_ciphertext_fails() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let p = params(&keys, Algorithm::AesGcm, 0, 0); + + let (mut encrypted, _) = + encrypt_segment(&p, b"data", "test.txt", &no_compression_config()).expect("encrypt"); + encrypted[NONCE_LEN + 1] ^= 0xFF; + + let result = decrypt_segment(&p, &encrypted, CompressionAlgorithm::None); + assert!(result.is_err()); + } + + // -- Segment compress+encrypt/decrypt+decompress ------------------------ + + #[cfg(feature = "compression")] + #[test] + fn test_segment_zstd_roundtrip() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"compressible data repeated ".repeat(100); + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + }; + let p = params(&keys, Algorithm::ChaCha20Poly1305, 5, 3); + + let (encrypted, effective) = + encrypt_segment(&p, &plaintext, "data.txt", &config).expect("encrypt"); + assert_eq!(effective, CompressionAlgorithm::Zstd); + + let decrypted = + decrypt_segment(&p, &encrypted, CompressionAlgorithm::Zstd).expect("decrypt"); + assert_eq!(decrypted, plaintext); + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_brotli_roundtrip() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"brotli test data ".repeat(80); + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: None, + }; + let p = params(&keys, Algorithm::AesGcm, 2, 1); + + let (encrypted, effective) = + encrypt_segment(&p, &plaintext, "notes.txt", &config).expect("encrypt"); + assert_eq!(effective, CompressionAlgorithm::Brotli); + + let decrypted = + decrypt_segment(&p, &encrypted, CompressionAlgorithm::Brotli).expect("decrypt"); + assert_eq!(decrypted, plaintext); + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_mime_skip() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"fake jpeg data"; + let config = CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + }; + let p = params(&keys, Algorithm::AesGcm, 0, 0); + + let (encrypted, effective) = + encrypt_segment(&p, plaintext, "photo.jpg", &config).expect("encrypt"); + assert_eq!(effective, CompressionAlgorithm::None); + + let decrypted = + decrypt_segment(&p, &encrypted, CompressionAlgorithm::None).expect("decrypt"); + assert_eq!(decrypted, plaintext); + } + + #[cfg(feature = "compression")] + #[test] + fn test_segment_compressed_smaller() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"aaaa".repeat(1000); + + let p0 = params(&keys, Algorithm::AesGcm, 0, 0); + let (compressed_enc, _) = encrypt_segment( + &p0, + &plaintext, + "text.txt", + &CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + }, + ) + .expect("compress+encrypt"); + + let p1 = params(&keys, Algorithm::AesGcm, 1, 0); + let (uncompressed_enc, _) = + encrypt_segment(&p1, &plaintext, "text.txt", &no_compression_config()) + .expect("encrypt only"); + + assert!(compressed_enc.len() < uncompressed_enc.len()); + } + + // -- Index encryption --------------------------------------------------- + + #[test] + fn test_index_encrypt_decrypt_roundtrip() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"index payload bytes for testing roundtrip"; + + let encrypted = + encrypt_index(keys.index_key.as_bytes(), Algorithm::AesGcm, 0, plaintext) + .expect("encrypt"); + + let decrypted = + decrypt_index(keys.index_key.as_bytes(), Algorithm::AesGcm, 0, &encrypted) + .expect("decrypt"); + + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_index_wrong_generation_fails() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"index data"; + + let encrypted = + encrypt_index(keys.index_key.as_bytes(), Algorithm::AesGcm, 5, plaintext) + .expect("encrypt"); + + // Decrypt with wrong generation + let result = decrypt_index(keys.index_key.as_bytes(), Algorithm::AesGcm, 6, &encrypted); + assert!(result.is_err()); + } + + #[test] + fn test_index_nonce_unique_per_generation() { + let keys = derive_vault_keys(&test_master_key()).expect("derive"); + let plaintext = b"same index data"; + + let enc0 = + encrypt_index(keys.index_key.as_bytes(), Algorithm::AesGcm, 0, plaintext) + .expect("encrypt gen 0"); + let enc1 = + encrypt_index(keys.index_key.as_bytes(), Algorithm::AesGcm, 1, plaintext) + .expect("encrypt gen 1"); + + // Nonce (first 12 bytes) must differ + assert_ne!(&enc0[..NONCE_LEN], &enc1[..NONCE_LEN]); + } + + // -- Checksums ---------------------------------------------------------- + + #[test] + fn test_checksum_roundtrip() { + let data = b"hello checksum"; + let checksum = compute_checksum(data); + assert!(verify_checksum(data, &checksum)); + } + + #[test] + fn test_checksum_tampered() { + let data = b"original data"; + let checksum = compute_checksum(data); + let mut tampered = data.to_vec(); + tampered[0] ^= 0xFF; + assert!(!verify_checksum(&tampered, &checksum)); + } + + #[cfg(feature = "compression")] + #[test] + fn test_checksum_covers_original_plaintext() { + let plaintext = b"checksum covers pre-compression data ".repeat(50); + let checksum = compute_checksum(&plaintext); + + // Compress the data + let compressed = compression::compress( + &plaintext, + &CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + }, + ) + .expect("compress"); + + // Checksum should NOT match the compressed form + assert!(!verify_checksum(&compressed, &checksum)); + + // Decompress and verify against original plaintext + let decompressed = + compression::decompress(&compressed, CompressionAlgorithm::Zstd).expect("decompress"); + assert!(verify_checksum(&decompressed, &checksum)); + } + + // -- Pre-allocation and secure erase ------------------------------------ + + #[test] + fn test_preallocate_size() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("vault.test"); + let mut file = std::fs::File::create(&path).expect("create"); + + preallocate_vault(&mut file, 4096).expect("preallocate"); + + let meta = std::fs::metadata(&path).expect("metadata"); + assert_eq!(meta.len(), 4096); + } + + #[test] + fn test_preallocate_random_fill() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("vault.test"); + let mut file = std::fs::File::create(&path).expect("create"); + + preallocate_vault(&mut file, 4096).expect("preallocate"); + + let data = std::fs::read(&path).expect("read"); + // Extremely unlikely all zeros if filled with CSPRNG + assert!(data.iter().any(|&b| b != 0)); + } + + #[test] + fn test_preallocate_streaming() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("vault_10mb.test"); + let mut file = std::fs::File::create(&path).expect("create"); + + let size = 10 * 1024 * 1024; // 10MB + preallocate_vault(&mut file, size).expect("preallocate 10MB"); + + let meta = std::fs::metadata(&path).expect("metadata"); + assert_eq!(meta.len(), size); + } + + #[test] + fn test_secure_erase_region() { + use std::io::Read; + + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("erase.test"); + + // Write known pattern + let pattern = vec![0xAA; 1024]; + std::fs::write(&path, &pattern).expect("write"); + + // Erase a region in the middle + let mut file = std::fs::OpenOptions::new() + .write(true) + .read(true) + .open(&path) + .expect("open"); + + secure_erase_region(&mut file, 256, 512).expect("erase"); + + // Read back the erased region + file.seek(SeekFrom::Start(256)).expect("seek"); + let mut erased = vec![0u8; 512]; + file.read_exact(&mut erased).expect("read"); + + // Should no longer be the original 0xAA pattern + assert_ne!(erased, vec![0xAA; 512]); + + // Regions outside the erased area should be untouched + file.seek(SeekFrom::Start(0)).expect("seek"); + let mut before = vec![0u8; 256]; + file.read_exact(&mut before).expect("read"); + assert_eq!(before, vec![0xAA; 256]); + + file.seek(SeekFrom::Start(768)).expect("seek"); + let mut after = vec![0u8; 256]; + file.read_exact(&mut after).expect("read"); + assert_eq!(after, vec![0xAA; 256]); + } +} diff --git a/rust/src/api/evfs/wal.rs b/rust/src/api/evfs/wal.rs new file mode 100644 index 0000000..e77d4c6 --- /dev/null +++ b/rust/src/api/evfs/wal.rs @@ -0,0 +1 @@ +//! Write-ahead log, crash recovery, and file locking. diff --git a/rust/src/api/hashing/mod.rs b/rust/src/api/hashing/mod.rs index 9846b05..d9618f7 100644 --- a/rust/src/api/hashing/mod.rs +++ b/rust/src/api/hashing/mod.rs @@ -23,6 +23,31 @@ impl HasherHandle { inner: Mutex::new(hasher), } } + + pub(crate) fn update_raw(&self, data: &[u8]) -> Result<(), CryptoError> { + let mut guard = self + .inner + .lock() + .map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?; + guard.update(data) + } + + #[allow(dead_code)] // used by streaming hash tests + pub(crate) fn finalize_raw(&self) -> Result, CryptoError> { + let guard = self + .inner + .lock() + .map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?; + guard.finalize() + } + + pub(crate) fn reset_raw(&self) -> Result<(), CryptoError> { + let mut guard = self + .inner + .lock() + .map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?; + guard.reset() + } } /// Create a BLAKE3 hasher handle. diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index 2b19395..8b5e9ad 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -1,6 +1,9 @@ //! Public API module (scanned by FRB for binding generation). +pub mod compression; pub mod encryption; pub mod error; +pub mod evfs; pub mod hashing; pub mod kdf; +pub mod streaming; diff --git a/rust/src/api/streaming/compress.rs b/rust/src/api/streaming/compress.rs new file mode 100644 index 0000000..4ccec03 --- /dev/null +++ b/rust/src/api/streaming/compress.rs @@ -0,0 +1,245 @@ +//! Stream-compress-then-chunk encryption and decrypt-then-decompress. + +use std::fs::{self, File}; +use std::io::{BufReader, BufWriter, Write}; + +use crate::core::compression::streaming::{new_compressor, new_decompressor}; +use crate::api::compression::{should_skip_compression, CompressionAlgorithm, CompressionConfig}; +use crate::api::encryption::CipherHandle; +use crate::core::error::CryptoError; +use crate::core::streaming::{ + finish_file, pad_last_chunk, strip_last_chunk_padding, ChunkAad, ChunkReader, ChunkWriter, + EncryptedChunk, StreamHeader, CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE, STREAM_HEADER_SIZE, +}; + +use super::{algorithm_from_id, open_input, parse_encrypted_output, read_full, reassemble_into, TempFileGuard}; + +/// Stream-compress, then chunk the compressed stream. +/// +/// Read input → feed to a streaming compressor → buffer compressed bytes → +/// when buffer >= CHUNK_SIZE, encrypt as intermediate chunk → at EOF, +/// finish compressor, pad only the remainder as the final chunk. +/// +/// Compressed data fills 64KB buckets naturally. A file that compresses 50% +/// produces half as many encrypted chunks. +pub(crate) fn compress_encrypt_file_impl( + cipher: &CipherHandle, + compression: &CompressionConfig, + input_path: &str, + output_path: &str, + on_progress: &dyn Fn(f64), +) -> Result<(), CryptoError> { + let algo = algorithm_from_id(cipher.algorithm_id())?; + + let effective_algo = if compression.algorithm != CompressionAlgorithm::None + && should_skip_compression(input_path) + { + log::info!( + "Skipping compression for '{}' (already-compressed format)", + input_path + ); + CompressionAlgorithm::None + } else { + compression.algorithm + }; + + let level = compression.level; + + let (mut reader, file_size) = open_input(input_path)?; + + let tmp_path = format!("{output_path}.tmp"); + let mut guard = TempFileGuard::new(tmp_path.clone()); + + let output_file = File::create(&tmp_path) + .map_err(|e| CryptoError::IoError(format!("Cannot create output '{tmp_path}': {e}")))?; + + let mut writer = ChunkWriter::new(BufWriter::new(output_file)); + writer.write_header(&StreamHeader::new(algo, effective_algo))?; + + let mut enc_chunk = EncryptedChunk::new(); + let mut read_buf = vec![0u8; CHUNK_SIZE]; + let mut comp_buf = Vec::with_capacity(CHUNK_SIZE * 2); + let mut chunk_index: u64 = 0; + let mut bytes_fed: u64 = 0; + + let mut compressor = new_compressor(effective_algo, level)?; + + let flush_full_chunks = + |comp_buf: &mut Vec, + chunk_index: &mut u64, + enc_chunk: &mut EncryptedChunk, + writer: &mut ChunkWriter>| + -> Result<(), CryptoError> { + while comp_buf.len() >= CHUNK_SIZE { + let aad = ChunkAad { + index: *chunk_index, + is_final: false, + } + .to_bytes(); + let encrypted = cipher.encrypt_raw(&comp_buf[..CHUNK_SIZE], &aad)?; + parse_encrypted_output(&encrypted, enc_chunk)?; + writer.write_chunk(enc_chunk)?; + + comp_buf.drain(..CHUNK_SIZE); + *chunk_index += 1; + } + Ok(()) + }; + + loop { + let bytes_read = read_full(&mut reader, &mut read_buf)?; + if bytes_read == 0 { + break; + } + + compressor.compress_chunk(&read_buf[..bytes_read], &mut comp_buf)?; + bytes_fed += bytes_read as u64; + + flush_full_chunks(&mut comp_buf, &mut chunk_index, &mut enc_chunk, &mut writer)?; + + if file_size > 0 { + on_progress((bytes_fed as f64 / file_size as f64).min(0.99)); + } + } + + compressor.finish(&mut comp_buf)?; + flush_full_chunks(&mut comp_buf, &mut chunk_index, &mut enc_chunk, &mut writer)?; + + let plaintext = pad_last_chunk(&comp_buf)?; + let aad = ChunkAad { + index: chunk_index, + is_final: true, + } + .to_bytes(); + let encrypted = cipher.encrypt_raw(&plaintext, &aad)?; + parse_encrypted_output(&encrypted, &mut enc_chunk)?; + writer.write_chunk(&enc_chunk)?; + on_progress(1.0); + + finish_file(writer)?; + + fs::rename(&tmp_path, output_path).map_err(|e| { + CryptoError::IoError(format!("Cannot rename '{tmp_path}' → '{output_path}': {e}")) + })?; + guard.defuse(); + + Ok(()) +} + +/// Decrypt then stream-decompress a file. +/// +/// Reads compression algorithm from the file header, then decrypts each +/// chunk and feeds the compressed bytes through a streaming decompressor. +/// Intermediate chunks are raw compressed segments; only the final chunk +/// has length-prefix padding. +pub(crate) fn decrypt_decompress_file_impl( + cipher: &CipherHandle, + input_path: &str, + output_path: &str, + on_progress: &dyn Fn(f64), +) -> Result<(), CryptoError> { + let input_file = File::open(input_path) + .map_err(|e| CryptoError::IoError(format!("Cannot open input '{input_path}': {e}")))?; + let file_size = input_file + .metadata() + .map_err(|e| CryptoError::IoError(format!("Cannot stat input: {e}")))? + .len(); + + let tmp_path = format!("{output_path}.tmp"); + let mut guard = TempFileGuard::new(tmp_path.clone()); + + let output_file = File::create(&tmp_path) + .map_err(|e| CryptoError::IoError(format!("Cannot create output '{tmp_path}': {e}")))?; + + let mut reader = ChunkReader::new(BufReader::new(input_file)); + let mut out_writer = BufWriter::new(output_file); + + let header = reader.read_header()?; + let expected_algo = algorithm_from_id(cipher.algorithm_id())?; + if header.algorithm != expected_algo { + return Err(CryptoError::InvalidParameter(format!( + "Algorithm mismatch: file uses {:?}, cipher is {}", + header.algorithm, + cipher.algorithm_id() + ))); + } + + let comp_algo = header.compression; + + let data_size = file_size.saturating_sub(STREAM_HEADER_SIZE as u64); + let estimated_chunks = if data_size > 0 { + data_size.div_ceil(ENCRYPTED_CHUNK_SIZE as u64) + } else { + 1 + }; + + // Read-ahead by one chunk so we know which chunk is final without + // trial decryption — each chunk is decrypted exactly once. + let mut current = EncryptedChunk::new(); + let mut next = EncryptedChunk::new(); + let mut wire_buf = Vec::with_capacity(ENCRYPTED_CHUNK_SIZE); + let mut decomp_buf = Vec::with_capacity(CHUNK_SIZE * 2); + let mut decompressor = new_decompressor(comp_algo)?; + let mut i: u64 = 0; + + if !reader.read_chunk(&mut current)? { + return Err(CryptoError::DecryptionFailed); + } + + loop { + let has_next = reader.read_chunk(&mut next)?; + + reassemble_into(¤t, &mut wire_buf); + + if !has_next { + // Current is the final chunk + let aad = ChunkAad { index: i, is_final: true }.to_bytes(); + let plaintext = cipher + .decrypt_raw(&wire_buf, &aad) + .map_err(|_| CryptoError::DecryptionFailed)?; + let data = strip_last_chunk_padding(&plaintext)?; + if !data.is_empty() { + decompressor.decompress_chunk(&data, &mut decomp_buf)?; + } + decompressor.finish(&mut decomp_buf)?; + if !decomp_buf.is_empty() { + out_writer + .write_all(&decomp_buf) + .map_err(|e| CryptoError::IoError(format!("Write failed: {e}")))?; + decomp_buf.clear(); + } + on_progress(1.0); + break; + } + + // Current is an intermediate chunk + let aad = ChunkAad { index: i, is_final: false }.to_bytes(); + let plaintext = cipher + .decrypt_raw(&wire_buf, &aad) + .map_err(|_| CryptoError::DecryptionFailed)?; + decompressor.decompress_chunk(&plaintext, &mut decomp_buf)?; + if !decomp_buf.is_empty() { + out_writer + .write_all(&decomp_buf) + .map_err(|e| CryptoError::IoError(format!("Write failed: {e}")))?; + decomp_buf.clear(); + } + + std::mem::swap(&mut current, &mut next); + i += 1; + on_progress((i as f64 / estimated_chunks.max(1) as f64).min(0.99)); + } + + let file = out_writer + .into_inner() + .map_err(|e| CryptoError::IoError(format!("Flush failed: {e}")))?; + file.sync_all() + .map_err(|e| CryptoError::IoError(format!("Failed to fsync: {e}")))?; + drop(file); + fs::rename(&tmp_path, output_path).map_err(|e| { + CryptoError::IoError(format!("Cannot rename '{tmp_path}' → '{output_path}': {e}")) + })?; + guard.defuse(); + + Ok(()) +} diff --git a/rust/src/api/streaming/decrypt.rs b/rust/src/api/streaming/decrypt.rs new file mode 100644 index 0000000..dd612df --- /dev/null +++ b/rust/src/api/streaming/decrypt.rs @@ -0,0 +1,115 @@ +//! Plain streaming decryption (no decompression). + +use std::fs::{self, File}; +use std::io::{BufReader, BufWriter, Write}; + +use crate::api::encryption::CipherHandle; +use crate::core::error::CryptoError; +use crate::core::streaming::{ + strip_last_chunk_padding, ChunkAad, ChunkReader, EncryptedChunk, ENCRYPTED_CHUNK_SIZE, + STREAM_HEADER_SIZE, +}; + +use super::{algorithm_from_id, reassemble_into, TempFileGuard}; + +/// Decrypt a streaming-encrypted file. +/// +/// Writes to a temporary file and renames atomically on success. +/// On any error (wrong key, tampered data, truncation) the output is deleted. +/// `on_progress` receives values from 0.0 to 1.0. +pub(crate) fn decrypt_file_impl( + cipher: &CipherHandle, + input_path: &str, + output_path: &str, + on_progress: &dyn Fn(f64), +) -> Result<(), CryptoError> { + let input_file = File::open(input_path) + .map_err(|e| CryptoError::IoError(format!("Cannot open input '{input_path}': {e}")))?; + let file_size = input_file + .metadata() + .map_err(|e| CryptoError::IoError(format!("Cannot stat input: {e}")))? + .len(); + + let tmp_path = format!("{output_path}.tmp"); + let mut guard = TempFileGuard::new(tmp_path.clone()); + + let output_file = File::create(&tmp_path) + .map_err(|e| CryptoError::IoError(format!("Cannot create output '{tmp_path}': {e}")))?; + + let mut reader = ChunkReader::new(BufReader::new(input_file)); + let mut out_writer = BufWriter::new(output_file); + + let header = reader.read_header()?; + let expected_algo = algorithm_from_id(cipher.algorithm_id())?; + if header.algorithm != expected_algo { + return Err(CryptoError::InvalidParameter(format!( + "Algorithm mismatch: file uses {:?}, cipher is {}", + header.algorithm, + cipher.algorithm_id() + ))); + } + + let data_size = file_size.saturating_sub(STREAM_HEADER_SIZE as u64); + let estimated_chunks = if data_size > 0 { + data_size.div_ceil(ENCRYPTED_CHUNK_SIZE as u64) + } else { + 1 + }; + + // Read-ahead by one chunk so we know which chunk is final without + // trial decryption — each chunk is decrypted exactly once. + let mut current = EncryptedChunk::new(); + let mut next = EncryptedChunk::new(); + let mut wire_buf = Vec::with_capacity(ENCRYPTED_CHUNK_SIZE); + let mut i: u64 = 0; + + if !reader.read_chunk(&mut current)? { + return Err(CryptoError::DecryptionFailed); + } + + loop { + let has_next = reader.read_chunk(&mut next)?; + + reassemble_into(¤t, &mut wire_buf); + + if !has_next { + // Current is the final chunk + let aad = ChunkAad { index: i, is_final: true }.to_bytes(); + let plaintext = cipher + .decrypt_raw(&wire_buf, &aad) + .map_err(|_| CryptoError::DecryptionFailed)?; + let real_data = strip_last_chunk_padding(&plaintext)?; + out_writer + .write_all(&real_data) + .map_err(|e| CryptoError::IoError(format!("Write failed: {e}")))?; + on_progress(1.0); + break; + } + + // Current is an intermediate chunk + let aad = ChunkAad { index: i, is_final: false }.to_bytes(); + let plaintext = cipher + .decrypt_raw(&wire_buf, &aad) + .map_err(|_| CryptoError::DecryptionFailed)?; + out_writer + .write_all(&plaintext) + .map_err(|e| CryptoError::IoError(format!("Write failed: {e}")))?; + + std::mem::swap(&mut current, &mut next); + i += 1; + on_progress((i as f64 / estimated_chunks.max(1) as f64).min(0.99)); + } + + let file = out_writer + .into_inner() + .map_err(|e| CryptoError::IoError(format!("Flush failed: {e}")))?; + file.sync_all() + .map_err(|e| CryptoError::IoError(format!("Failed to fsync: {e}")))?; + drop(file); + fs::rename(&tmp_path, output_path).map_err(|e| { + CryptoError::IoError(format!("Cannot rename '{tmp_path}' → '{output_path}': {e}")) + })?; + guard.defuse(); + + Ok(()) +} diff --git a/rust/src/api/streaming/encrypt.rs b/rust/src/api/streaming/encrypt.rs new file mode 100644 index 0000000..5687e68 --- /dev/null +++ b/rust/src/api/streaming/encrypt.rs @@ -0,0 +1,106 @@ +//! Plain streaming encryption (no compression). + +use std::fs::File; +use std::io::BufWriter; + +use crate::api::compression::CompressionAlgorithm; +use crate::api::encryption::CipherHandle; +use crate::core::error::CryptoError; +use crate::core::streaming::{ + finish_file, pad_last_chunk, ChunkAad, ChunkWriter, EncryptedChunk, StreamHeader, CHUNK_SIZE, +}; + +use super::{algorithm_from_id, open_input, parse_encrypted_output, read_full, TempFileGuard}; + +/// Encrypt a file in streaming 64KB chunks. +/// +/// Writes to a temporary file and renames atomically on success. +/// `on_progress` receives values from 0.0 to 1.0. The final 1.0 is sent +/// before fsync — callers must check the `Result` for true completion. +pub(crate) fn encrypt_file_impl( + cipher: &CipherHandle, + input_path: &str, + output_path: &str, + on_progress: &dyn Fn(f64), +) -> Result<(), CryptoError> { + let algo = algorithm_from_id(cipher.algorithm_id())?; + + let (mut reader, file_size) = open_input(input_path)?; + + let tmp_path = format!("{output_path}.tmp"); + let mut guard = TempFileGuard::new(tmp_path.clone()); + + let output_file = File::create(&tmp_path) + .map_err(|e| CryptoError::IoError(format!("Cannot create output '{tmp_path}': {e}")))?; + + let mut writer = ChunkWriter::new(BufWriter::new(output_file)); + writer.write_header(&StreamHeader::new(algo, CompressionAlgorithm::None))?; + + let mut enc_chunk = EncryptedChunk::new(); + let mut read_buf = vec![0u8; CHUNK_SIZE]; + let mut chunk_index: u64 = 0; + + let total_chunks = if file_size == 0 { + 1u64 + } else { + let base = file_size.div_ceil(CHUNK_SIZE as u64); + if file_size % CHUNK_SIZE as u64 == 0 { + base + 1 + } else { + base + } + }; + + loop { + let bytes_read = read_full(&mut reader, &mut read_buf)?; + + if bytes_read == 0 { + let plaintext = pad_last_chunk(&[])?; + let aad = ChunkAad { + index: chunk_index, + is_final: true, + } + .to_bytes(); + let encrypted = cipher.encrypt_raw(&plaintext, &aad)?; + parse_encrypted_output(&encrypted, &mut enc_chunk)?; + writer.write_chunk(&enc_chunk)?; + on_progress(1.0); + break; + } + + if bytes_read < CHUNK_SIZE { + let plaintext = pad_last_chunk(&read_buf[..bytes_read])?; + let aad = ChunkAad { + index: chunk_index, + is_final: true, + } + .to_bytes(); + let encrypted = cipher.encrypt_raw(&plaintext, &aad)?; + parse_encrypted_output(&encrypted, &mut enc_chunk)?; + writer.write_chunk(&enc_chunk)?; + on_progress(1.0); + break; + } + + let aad = ChunkAad { + index: chunk_index, + is_final: false, + } + .to_bytes(); + let encrypted = cipher.encrypt_raw(&read_buf[..CHUNK_SIZE], &aad)?; + parse_encrypted_output(&encrypted, &mut enc_chunk)?; + writer.write_chunk(&enc_chunk)?; + + chunk_index += 1; + on_progress((chunk_index as f64 / total_chunks as f64).min(0.99)); + } + + finish_file(writer)?; + + std::fs::rename(&tmp_path, output_path).map_err(|e| { + CryptoError::IoError(format!("Cannot rename '{tmp_path}' → '{output_path}': {e}")) + })?; + guard.defuse(); + + Ok(()) +} diff --git a/rust/src/api/streaming/hash.rs b/rust/src/api/streaming/hash.rs new file mode 100644 index 0000000..2cf8b3f --- /dev/null +++ b/rust/src/api/streaming/hash.rs @@ -0,0 +1,62 @@ +//! Streaming file hashing (no encryption padding). + +use std::fs::File; +use std::io::BufReader; + +use crate::api::hashing::HasherHandle; +use crate::core::error::CryptoError; +use crate::core::streaming::CHUNK_SIZE; + +use super::read_full; + +/// Feed an entire file into the hasher in 64KB chunks. Does NOT finalize. +/// +/// Resets the hasher first to ensure a clean state, then feeds raw file bytes +/// (no padding). Caller must call `finalize_raw()` / `hasherFinalize()` to +/// obtain the digest. +pub(crate) fn hash_file_feed( + hasher: &HasherHandle, + file_path: &str, + on_progress: &dyn Fn(f64), +) -> Result<(), CryptoError> { + hasher.reset_raw()?; + + let file = File::open(file_path) + .map_err(|e| CryptoError::IoError(format!("Cannot open input '{file_path}': {e}")))?; + let file_size = file + .metadata() + .map_err(|e| CryptoError::IoError(format!("Cannot stat input: {e}")))? + .len(); + + let mut reader = BufReader::new(file); + let mut buf = vec![0u8; CHUNK_SIZE]; + let mut bytes_hashed: u64 = 0; + + loop { + let n = read_full(&mut reader, &mut buf)?; + if n == 0 { + break; + } + hasher.update_raw(&buf[..n])?; + bytes_hashed += n as u64; + if file_size > 0 { + on_progress((bytes_hashed as f64 / file_size as f64).min(0.99)); + } + } + + on_progress(1.0); + Ok(()) +} + +/// Hash a file in streaming 64KB chunks (feed + finalize). +/// +/// Convenience wrapper: feeds the entire file then finalizes. +#[cfg(test)] +pub(crate) fn hash_file_impl( + hasher: &HasherHandle, + file_path: &str, + on_progress: &dyn Fn(f64), +) -> Result, CryptoError> { + hash_file_feed(hasher, file_path, on_progress)?; + hasher.finalize_raw() +} diff --git a/rust/src/api/streaming/mod.rs b/rust/src/api/streaming/mod.rs new file mode 100644 index 0000000..580320d --- /dev/null +++ b/rust/src/api/streaming/mod.rs @@ -0,0 +1,184 @@ +//! Streaming file encryption, decryption, compression, and hashing API. +//! +//! Core logic uses a progress callback closure so it's testable without FRB. +//! The public FRB-visible functions are thin wrappers that forward progress +//! to a `StreamSink`. +//! +//! Both encrypt and decrypt write to a temporary file first, then atomically +//! rename on success. On any error the partial output is deleted — a failed +//! decryption never leaves plaintext on disk. + +#[cfg(feature = "compression")] +mod compress; +mod decrypt; +mod encrypt; +pub(crate) mod hash; + +#[cfg(test)] +mod tests; + +use std::fs::{self, File}; +use std::io::{BufReader, Read}; + +#[cfg(feature = "compression")] +use crate::api::compression::CompressionConfig; +use crate::api::encryption::CipherHandle; +use crate::api::hashing::HasherHandle; +use crate::core::error::CryptoError; +use crate::core::streaming::{ + EncryptedChunk, StreamAlgorithm, CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE, NONCE_SIZE, +}; + +// Re-export impl functions for internal use and tests +pub(crate) use decrypt::decrypt_file_impl; +pub(crate) use encrypt::encrypt_file_impl; +pub(crate) use hash::hash_file_feed; +#[cfg(feature = "compression")] +pub(crate) use compress::{compress_encrypt_file_impl, decrypt_decompress_file_impl}; + +// -- Shared helpers ----------------------------------------------------------- + +fn algorithm_from_id(id: &str) -> Result { + match id { + "aes-256-gcm" => Ok(StreamAlgorithm::AesGcm), + "chacha20-poly1305" => Ok(StreamAlgorithm::ChaCha20Poly1305), + other => Err(CryptoError::InvalidParameter(format!( + "Algorithm '{other}' not supported for streaming" + ))), + } +} + +fn parse_encrypted_output(data: &[u8], chunk: &mut EncryptedChunk) -> Result<(), CryptoError> { + if data.len() != ENCRYPTED_CHUNK_SIZE { + return Err(CryptoError::InvalidParameter(format!( + "Cipher output wrong size: {} bytes, expected {ENCRYPTED_CHUNK_SIZE}", + data.len() + ))); + } + chunk.nonce.copy_from_slice(&data[..NONCE_SIZE]); + chunk.ciphertext[..CHUNK_SIZE].copy_from_slice(&data[NONCE_SIZE..NONCE_SIZE + CHUNK_SIZE]); + chunk.tag.copy_from_slice(&data[NONCE_SIZE + CHUNK_SIZE..]); + Ok(()) +} + +/// Reassemble nonce || ciphertext || tag into `buf` for decryption. +fn reassemble_into(chunk: &EncryptedChunk, buf: &mut Vec) { + buf.clear(); + buf.extend_from_slice(&chunk.nonce); + buf.extend_from_slice(&chunk.ciphertext); + buf.extend_from_slice(&chunk.tag); +} + +/// Read exactly `buf.len()` bytes, tolerating partial reads and EINTR. +fn read_full(reader: &mut R, buf: &mut [u8]) -> Result { + let mut offset = 0; + loop { + match reader.read(&mut buf[offset..]) { + Ok(0) => return Ok(offset), + Ok(n) => { + offset += n; + if offset == buf.len() { + return Ok(offset); + } + } + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(CryptoError::IoError(format!("Read failed: {e}"))), + } + } +} + +/// Open an input file and return a buffered reader + file size. +fn open_input(path: &str) -> Result<(BufReader, u64), CryptoError> { + let file = File::open(path) + .map_err(|e| CryptoError::IoError(format!("Cannot open input '{path}': {e}")))?; + let size = file + .metadata() + .map_err(|e| CryptoError::IoError(format!("Cannot stat input: {e}")))? + .len(); + Ok((BufReader::new(file), size)) +} + +/// Drop guard that removes a temporary file unless `defuse()` is called. +struct TempFileGuard { + path: String, + active: bool, +} + +impl TempFileGuard { + fn new(path: String) -> Self { + Self { path, active: true } + } + + fn defuse(&mut self) { + self.active = false; + } +} + +impl Drop for TempFileGuard { + fn drop(&mut self) { + if self.active { + let _ = fs::remove_file(&self.path); + } + } +} + +// -- FRB entry points (thin wrappers) ---------------------------------------- + +use crate::frb_generated::StreamSink; + +pub fn stream_encrypt_file( + cipher: &CipherHandle, + input_path: String, + output_path: String, + progress_sink: StreamSink, +) -> Result<(), CryptoError> { + encrypt_file_impl(cipher, &input_path, &output_path, &|p| { + let _ = progress_sink.add(p); + }) +} + +pub fn stream_decrypt_file( + cipher: &CipherHandle, + input_path: String, + output_path: String, + progress_sink: StreamSink, +) -> Result<(), CryptoError> { + decrypt_file_impl(cipher, &input_path, &output_path, &|p| { + let _ = progress_sink.add(p); + }) +} + +#[cfg(feature = "compression")] +pub fn stream_compress_encrypt_file( + cipher: &CipherHandle, + compression: CompressionConfig, + input_path: String, + output_path: String, + progress_sink: StreamSink, +) -> Result<(), CryptoError> { + compress_encrypt_file_impl(cipher, &compression, &input_path, &output_path, &|p| { + let _ = progress_sink.add(p); + }) +} + +#[cfg(feature = "compression")] +pub fn stream_decrypt_decompress_file( + cipher: &CipherHandle, + input_path: String, + output_path: String, + progress_sink: StreamSink, +) -> Result<(), CryptoError> { + decrypt_decompress_file_impl(cipher, &input_path, &output_path, &|p| { + let _ = progress_sink.add(p); + }) +} + +pub fn stream_hash_file( + hasher: &HasherHandle, + file_path: String, + progress_sink: StreamSink, +) -> Result<(), CryptoError> { + hash_file_feed(hasher, &file_path, &|p| { + let _ = progress_sink.add(p); + }) +} diff --git a/rust/src/api/streaming/tests.rs b/rust/src/api/streaming/tests.rs new file mode 100644 index 0000000..3b06486 --- /dev/null +++ b/rust/src/api/streaming/tests.rs @@ -0,0 +1,734 @@ +//! Tests for the streaming encryption/decryption/compression/hashing pipeline. + +use super::*; +#[cfg(feature = "compression")] +use crate::api::compression::CompressionAlgorithm; +use crate::api::encryption::{create_aes256_gcm, generate_aes256_gcm_key}; +use crate::api::encryption::{create_chacha20_poly1305, generate_chacha20_poly1305_key}; +use crate::core::streaming::{ + ChunkReader, EncryptedChunk, CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE, STREAM_HEADER_SIZE, +}; +use std::fs; +use std::fs::File; +use std::io::BufReader; + +fn make_aes_cipher() -> CipherHandle { + let key = generate_aes256_gcm_key().expect("keygen"); + create_aes256_gcm(key).expect("cipher") +} + +fn make_chacha_cipher() -> CipherHandle { + let key = generate_chacha20_poly1305_key().expect("keygen"); + create_chacha20_poly1305(key).expect("cipher") +} + +fn noop_progress(_: f64) {} + +/// Encrypt -> decrypt -> compare bytes. +fn roundtrip_test(cipher: &CipherHandle, original: &[u8]) { + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, original).expect("write input"); + + encrypt_file_impl( + cipher, + input.to_str().expect("path"), + encrypted.to_str().expect("path"), + &noop_progress, + ) + .expect("encrypt"); + + // Verify uniform chunk sizes + let enc_size = fs::metadata(&encrypted).expect("stat").len() as usize; + let data_portion = enc_size - STREAM_HEADER_SIZE; + assert_eq!(data_portion % ENCRYPTED_CHUNK_SIZE, 0, "Chunks not uniform"); + + decrypt_file_impl( + cipher, + encrypted.to_str().expect("path"), + decrypted.to_str().expect("path"), + &noop_progress, + ) + .expect("decrypt"); + + let result = fs::read(&decrypted).expect("read output"); + assert_eq!(result, original, "Roundtrip mismatch"); +} + +#[test] +fn test_encrypt_decrypt_roundtrip() { + roundtrip_test(&make_aes_cipher(), b"Hello, streaming encryption!"); +} + +#[test] +fn test_small_file() { + roundtrip_test(&make_aes_cipher(), &[0x42; 100]); +} + +#[test] +fn test_exact_chunk_boundary() { + roundtrip_test(&make_aes_cipher(), &vec![0xAB; CHUNK_SIZE]); +} + +#[test] +fn test_multi_chunk() { + roundtrip_test(&make_aes_cipher(), &vec![0xCD; 200 * 1024]); +} + +#[test] +fn test_empty_file() { + roundtrip_test(&make_aes_cipher(), &[]); +} + +#[test] +fn test_chacha20_roundtrip() { + roundtrip_test(&make_chacha_cipher(), b"ChaCha20 streaming test data"); +} + +#[test] +fn test_wrong_key_fails() { + let cipher1 = make_aes_cipher(); + let cipher2 = make_aes_cipher(); + + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, b"secret data").expect("write"); + encrypt_file_impl( + &cipher1, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + let result = decrypt_file_impl( + &cipher2, + encrypted.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ); + assert!(result.is_err(), "Should fail with wrong key"); +} + +#[test] +fn test_wrong_key_leaves_no_output() { + let cipher1 = make_aes_cipher(); + let cipher2 = make_aes_cipher(); + + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, b"secret data").expect("write"); + encrypt_file_impl( + &cipher1, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + let _ = decrypt_file_impl( + &cipher2, + encrypted.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ); + + assert!( + !decrypted.exists(), + "Output file should not exist after failed decrypt" + ); + let tmp = format!("{}.tmp", decrypted.to_str().expect("p")); + assert!( + !std::path::Path::new(&tmp).exists(), + "Temp file should be cleaned up" + ); +} + +#[test] +fn test_chunk_reorder_detected() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let tampered = dir.path().join("tampered.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, vec![0xAA; CHUNK_SIZE * 3]).expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + // Swap chunk 0 and chunk 1 + let mut data = fs::read(&encrypted).expect("read"); + let c0 = STREAM_HEADER_SIZE; + let c1 = c0 + ENCRYPTED_CHUNK_SIZE; + let chunk0: Vec = data[c0..c1].to_vec(); + let chunk1: Vec = data[c1..c1 + ENCRYPTED_CHUNK_SIZE].to_vec(); + data[c0..c1].copy_from_slice(&chunk1); + data[c1..c1 + ENCRYPTED_CHUNK_SIZE].copy_from_slice(&chunk0); + fs::write(&tampered, &data).expect("write tampered"); + + let result = decrypt_file_impl( + &cipher, + tampered.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ); + assert!(result.is_err(), "Should detect chunk reorder"); +} + +#[test] +fn test_chunk_deletion_detected() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let tampered = dir.path().join("tampered.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, vec![0xBB; CHUNK_SIZE * 3]).expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + // Remove the middle chunk + let data = fs::read(&encrypted).expect("read"); + let mut deleted = Vec::new(); + deleted.extend_from_slice(&data[..STREAM_HEADER_SIZE + ENCRYPTED_CHUNK_SIZE]); + deleted.extend_from_slice(&data[STREAM_HEADER_SIZE + 2 * ENCRYPTED_CHUNK_SIZE..]); + fs::write(&tampered, &deleted).expect("write tampered"); + + let result = decrypt_file_impl( + &cipher, + tampered.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ); + assert!(result.is_err(), "Should detect chunk deletion"); +} + +#[test] +fn test_truncation_detected() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let tampered = dir.path().join("tampered.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, vec![0xCC; CHUNK_SIZE + 100]).expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + // Keep only header + first chunk (remove final chunk) + let data = fs::read(&encrypted).expect("read"); + let truncated = &data[..STREAM_HEADER_SIZE + ENCRYPTED_CHUNK_SIZE]; + fs::write(&tampered, truncated).expect("write tampered"); + + let result = decrypt_file_impl( + &cipher, + tampered.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ); + assert!(result.is_err(), "Should detect truncation"); +} + +#[test] +fn test_uniform_chunk_sizes() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + + fs::write(&input, [0xDD; 100]).expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + let enc_size = fs::metadata(&encrypted).expect("stat").len() as usize; + let data_portion = enc_size - STREAM_HEADER_SIZE; + assert_eq!(data_portion, ENCRYPTED_CHUNK_SIZE); +} + +#[test] +fn test_padding_stripped_correctly() { + let cipher = make_aes_cipher(); + let original = vec![0xEE; 100]; + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, &original).expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + decrypt_file_impl( + &cipher, + encrypted.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("decrypt"); + + let result = fs::read(&decrypted).expect("read"); + assert_eq!(result.len(), 100); + assert_eq!(result, original); +} + +#[test] +fn test_progress_values() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + // 3 full chunks -> 3 intermediate + 1 empty final = 4 total + fs::write(&input, vec![0xAA; CHUNK_SIZE * 3]).expect("write"); + + let enc_progress = std::sync::Mutex::new(Vec::new()); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &|p| enc_progress.lock().expect("lock").push(p), + ) + .expect("encrypt"); + + let vals = enc_progress.lock().expect("lock"); + assert!(!vals.is_empty()); + assert!((vals.last().copied().unwrap_or(0.0) - 1.0).abs() < f64::EPSILON); + for &v in &vals[..vals.len() - 1] { + assert!(v < 1.0, "Intermediate progress should be < 1.0, got {v}"); + } + + let dec_progress = std::sync::Mutex::new(Vec::new()); + decrypt_file_impl( + &cipher, + encrypted.to_str().expect("p"), + decrypted.to_str().expect("p"), + &|p| dec_progress.lock().expect("lock").push(p), + ) + .expect("decrypt"); + + let vals = dec_progress.lock().expect("lock"); + assert!(!vals.is_empty()); + assert!((vals.last().copied().unwrap_or(0.0) - 1.0).abs() < f64::EPSILON); +} + +#[test] +fn test_chunk_reuse_no_allocation() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + + fs::write(&input, vec![0xFF; CHUNK_SIZE * 3]).expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + let file = File::open(&encrypted).expect("open"); + let mut reader = ChunkReader::new(BufReader::new(file)); + let _header = reader.read_header().expect("header"); + + let mut chunk = EncryptedChunk::new(); + let chunk_ptr = chunk.ciphertext.as_ptr(); + + let mut count = 0; + while reader.read_chunk(&mut chunk).expect("read") { + assert_eq!( + chunk.ciphertext.as_ptr(), + chunk_ptr, + "Chunk buffer was reallocated" + ); + count += 1; + } + assert!(count > 0); +} + +#[test] +fn test_no_temp_file_left_on_success() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + + fs::write(&input, b"data").expect("write"); + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + encrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("encrypt"); + + assert!(encrypted.exists(), "Output should exist"); + let tmp = format!("{}.tmp", encrypted.to_str().expect("p")); + assert!( + !std::path::Path::new(&tmp).exists(), + "Temp file should be gone after success" + ); +} + +// -- Streaming hash tests ----------------------------------------------------- + +fn make_blake3_hasher() -> HasherHandle { + crate::api::hashing::create_blake3() +} + +fn make_sha3_hasher() -> HasherHandle { + crate::api::hashing::create_sha3() +} + +#[test] +fn test_streaming_hash_matches_oneshot_blake3() { + let data = b"Hello, streaming hash with BLAKE3!"; + let dir = tempfile::tempdir().expect("tmpdir"); + let path = dir.path().join("input.bin"); + fs::write(&path, data).expect("write"); + + let hasher = make_blake3_hasher(); + let digest = + hash::hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress).expect("hash"); + + let oneshot = crate::api::hashing::blake3_hash(data.to_vec()); + assert_eq!(digest, oneshot); +} + +#[test] +fn test_streaming_hash_matches_oneshot_sha3() { + let data = b"Hello, streaming hash with SHA-3!"; + let dir = tempfile::tempdir().expect("tmpdir"); + let path = dir.path().join("input.bin"); + fs::write(&path, data).expect("write"); + + let hasher = make_sha3_hasher(); + let digest = + hash::hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress).expect("hash"); + + let oneshot = crate::api::hashing::sha3_hash(data.to_vec()); + assert_eq!(digest, oneshot); +} + +#[test] +fn test_streaming_hash_empty_file() { + let dir = tempfile::tempdir().expect("tmpdir"); + let path = dir.path().join("empty.bin"); + fs::write(&path, b"").expect("write"); + + let hasher = make_blake3_hasher(); + let digest = + hash::hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress).expect("hash"); + + let oneshot = crate::api::hashing::blake3_hash(Vec::new()); + assert_eq!(digest, oneshot); +} + +#[test] +fn test_streaming_hash_large_file() { + let data = vec![0xAB; 1024 * 1024 + 37]; + let dir = tempfile::tempdir().expect("tmpdir"); + let path = dir.path().join("large.bin"); + fs::write(&path, &data).expect("write"); + + let hasher = make_blake3_hasher(); + let digest = + hash::hash_file_impl(&hasher, path.to_str().expect("p"), &noop_progress).expect("hash"); + + let oneshot = crate::api::hashing::blake3_hash(data); + assert_eq!(digest, oneshot); +} + +#[test] +fn test_streaming_hash_exact_boundary() { + let data = vec![0xCD; CHUNK_SIZE]; + let dir = tempfile::tempdir().expect("tmpdir"); + let path = dir.path().join("boundary.bin"); + fs::write(&path, &data).expect("write"); + + let hasher = make_blake3_hasher(); + let progress = std::sync::Mutex::new(Vec::new()); + let digest = hash::hash_file_impl(&hasher, path.to_str().expect("p"), &|p| { + progress.lock().expect("lock").push(p) + }) + .expect("hash"); + + let oneshot = crate::api::hashing::blake3_hash(data); + assert_eq!(digest, oneshot); + + let vals = progress.lock().expect("lock"); + assert!(!vals.is_empty()); + assert!((vals.last().copied().unwrap_or(0.0) - 1.0).abs() < f64::EPSILON); +} + +// -- Streaming compression tests ---------------------------------------------- + +#[cfg(feature = "compression")] +use crate::api::compression::CompressionConfig; + +#[cfg(feature = "compression")] +fn zstd_config() -> CompressionConfig { + CompressionConfig { + algorithm: CompressionAlgorithm::Zstd, + level: None, + } +} + +#[cfg(feature = "compression")] +fn brotli_config() -> CompressionConfig { + CompressionConfig { + algorithm: CompressionAlgorithm::Brotli, + level: None, + } +} + +#[cfg(feature = "compression")] +fn none_config() -> CompressionConfig { + CompressionConfig { + algorithm: CompressionAlgorithm::None, + level: None, + } +} + +/// Helper: compress-encrypt → decrypt-decompress → compare. +#[cfg(feature = "compression")] +fn compress_roundtrip_test(cipher: &CipherHandle, config: &CompressionConfig, original: &[u8]) { + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, original).expect("write input"); + + compress_encrypt_file_impl( + cipher, + config, + input.to_str().expect("path"), + encrypted.to_str().expect("path"), + &noop_progress, + ) + .expect("compress+encrypt"); + + decrypt_decompress_file_impl( + cipher, + encrypted.to_str().expect("path"), + decrypted.to_str().expect("path"), + &noop_progress, + ) + .expect("decrypt+decompress"); + + let result = fs::read(&decrypted).expect("read output"); + assert_eq!(result, original, "Roundtrip mismatch"); +} + +#[cfg(feature = "compression")] +#[test] +fn test_compress_encrypt_decrypt_decompress_roundtrip_zstd() { + let cipher = make_aes_cipher(); + compress_roundtrip_test(&cipher, &zstd_config(), b"Hello, Zstd streaming roundtrip!"); + compress_roundtrip_test(&cipher, &zstd_config(), &vec![0xAB; 200 * 1024]); + compress_roundtrip_test(&cipher, &zstd_config(), &[]); + compress_roundtrip_test(&cipher, &zstd_config(), &vec![0xFE; CHUNK_SIZE]); + compress_roundtrip_test(&cipher, &zstd_config(), &vec![0xFE; CHUNK_SIZE * 3]); +} + +#[cfg(feature = "compression")] +#[test] +fn test_compress_encrypt_decrypt_decompress_roundtrip_brotli() { + let cipher = make_aes_cipher(); + compress_roundtrip_test( + &cipher, + &brotli_config(), + b"Hello, Brotli streaming roundtrip!", + ); + compress_roundtrip_test(&cipher, &brotli_config(), &vec![0xCD; 200 * 1024]); + compress_roundtrip_test(&cipher, &brotli_config(), &[]); + compress_roundtrip_test(&cipher, &brotli_config(), &vec![0xFE; CHUNK_SIZE]); +} + +#[cfg(feature = "compression")] +#[test] +fn test_mime_skip_jpg() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("photo.jpg"); + let encrypted = dir.path().join("encrypted.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + let original = vec![0xFF; 1024]; + fs::write(&input, &original).expect("write input"); + + compress_encrypt_file_impl( + &cipher, + &zstd_config(), + input.to_str().expect("path"), + encrypted.to_str().expect("path"), + &noop_progress, + ) + .expect("compress+encrypt"); + + let enc_bytes = fs::read(&encrypted).expect("read encrypted"); + assert_eq!( + enc_bytes[8], 0x00, + "Header compression byte should be 0x00 (None) for .jpg" + ); + + decrypt_decompress_file_impl( + &cipher, + encrypted.to_str().expect("path"), + decrypted.to_str().expect("path"), + &noop_progress, + ) + .expect("decrypt+decompress"); + + let result = fs::read(&decrypted).expect("read output"); + assert_eq!(result, original); +} + +#[cfg(feature = "compression")] +#[test] +fn test_compression_none_matches_plain_encrypt() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + let input = dir.path().join("input.bin"); + let enc_plain = dir.path().join("enc_plain.bin"); + let enc_comp = dir.path().join("enc_comp.bin"); + let dec_plain = dir.path().join("dec_plain.bin"); + let dec_comp = dir.path().join("dec_comp.bin"); + + let original = b"Test data for None comparison"; + fs::write(&input, original).expect("write input"); + + encrypt_file_impl( + &cipher, + input.to_str().expect("path"), + enc_plain.to_str().expect("path"), + &noop_progress, + ) + .expect("plain encrypt"); + + compress_encrypt_file_impl( + &cipher, + &none_config(), + input.to_str().expect("path"), + enc_comp.to_str().expect("path"), + &noop_progress, + ) + .expect("none compress+encrypt"); + + let plain_size = fs::metadata(&enc_plain).expect("stat").len(); + let comp_size = fs::metadata(&enc_comp).expect("stat").len(); + assert_eq!( + plain_size, comp_size, + "None compression should produce same size as plain encrypt" + ); + + decrypt_file_impl( + &cipher, + enc_plain.to_str().expect("path"), + dec_plain.to_str().expect("path"), + &noop_progress, + ) + .expect("plain decrypt"); + + decrypt_decompress_file_impl( + &cipher, + enc_comp.to_str().expect("path"), + dec_comp.to_str().expect("path"), + &noop_progress, + ) + .expect("none decrypt+decompress"); + + assert_eq!(fs::read(&dec_plain).expect("read"), original); + assert_eq!(fs::read(&dec_comp).expect("read"), original); +} + +#[cfg(feature = "compression")] +#[test] +fn test_compressed_file_fewer_chunks() { + let cipher = make_aes_cipher(); + let dir = tempfile::tempdir().expect("tmpdir"); + + let original = vec![b'A'; CHUNK_SIZE * 4]; + let input = dir.path().join("input.bin"); + let enc_plain = dir.path().join("enc_plain.bin"); + let enc_comp = dir.path().join("enc_comp.bin"); + let decrypted = dir.path().join("decrypted.bin"); + + fs::write(&input, &original).expect("write"); + + encrypt_file_impl( + &cipher, + input.to_str().expect("p"), + enc_plain.to_str().expect("p"), + &noop_progress, + ) + .expect("plain encrypt"); + + let plain_size = fs::metadata(&enc_plain).expect("stat").len() as usize; + let plain_chunks = (plain_size - STREAM_HEADER_SIZE) / ENCRYPTED_CHUNK_SIZE; + + compress_encrypt_file_impl( + &cipher, + &zstd_config(), + input.to_str().expect("p"), + enc_comp.to_str().expect("p"), + &noop_progress, + ) + .expect("compress+encrypt"); + + let comp_size = fs::metadata(&enc_comp).expect("stat").len() as usize; + let comp_chunks = (comp_size - STREAM_HEADER_SIZE) / ENCRYPTED_CHUNK_SIZE; + + assert!( + comp_chunks < plain_chunks, + "Compressed should use fewer chunks: {comp_chunks} vs {plain_chunks}" + ); + + decrypt_decompress_file_impl( + &cipher, + enc_comp.to_str().expect("p"), + decrypted.to_str().expect("p"), + &noop_progress, + ) + .expect("decrypt+decompress"); + + assert_eq!(fs::read(&decrypted).expect("read"), original); +} diff --git a/rust/src/core/compression/brotli_impl.rs b/rust/src/core/compression/brotli_impl.rs new file mode 100644 index 0000000..1969050 --- /dev/null +++ b/rust/src/core/compression/brotli_impl.rs @@ -0,0 +1,39 @@ +//! Brotli compression wrapper. + +use crate::core::error::CryptoError; + +pub const DEFAULT_LEVEL: u32 = 4; +pub const MIN_LEVEL: u32 = 0; +pub const MAX_LEVEL: u32 = 11; + +pub fn validate_level(level: u32) -> Result<(), CryptoError> { + if level > MAX_LEVEL { + return Err(CryptoError::InvalidParameter(format!( + "Brotli level must be {MIN_LEVEL}–{MAX_LEVEL}, got {level}" + ))); + } + Ok(()) +} + +pub fn compress(data: &[u8], level: u32) -> Result, CryptoError> { + validate_level(level)?; + let mut output = Vec::new(); + { + let params = brotli::enc::BrotliEncoderParams { + quality: level as i32, + ..Default::default() + }; + let mut writer = brotli::CompressorWriter::with_params(&mut output, 4096, ¶ms); + std::io::Write::write_all(&mut writer, data) + .map_err(|e| CryptoError::CompressionFailed(e.to_string()))?; + } + Ok(output) +} + +pub fn decompress(data: &[u8]) -> Result, CryptoError> { + let mut output = Vec::new(); + let mut reader = brotli::Decompressor::new(std::io::Cursor::new(data), 4096); + std::io::Read::read_to_end(&mut reader, &mut output) + .map_err(|e| CryptoError::CompressionFailed(e.to_string()))?; + Ok(output) +} diff --git a/rust/src/core/compression/mod.rs b/rust/src/core/compression/mod.rs new file mode 100644 index 0000000..0049149 --- /dev/null +++ b/rust/src/core/compression/mod.rs @@ -0,0 +1,8 @@ +//! Internal compression implementations (not exposed to FRB). + +#[cfg(feature = "compression")] +pub mod brotli_impl; +#[cfg(feature = "compression")] +pub mod streaming; +#[cfg(feature = "compression")] +pub mod zstd_impl; diff --git a/rust/src/core/compression/streaming.rs b/rust/src/core/compression/streaming.rs new file mode 100644 index 0000000..f5692eb --- /dev/null +++ b/rust/src/core/compression/streaming.rs @@ -0,0 +1,378 @@ +//! Streaming compression/decompression wrappers for the chunk pipeline. +//! +//! These wrap zstd and brotli in a uniform interface that accepts incremental +//! input and appends compressed/decompressed bytes to an output buffer. + +use std::io::Write; + +use crate::api::compression::CompressionAlgorithm; +use crate::core::error::CryptoError; +use crate::core::streaming::CHUNK_SIZE; + +// 16 MiB per chunk — a 64KB compressed chunk expanding beyond this +// ratio (256:1) is almost certainly a decompression bomb. +const MAX_DECOMP_PER_CHUNK: usize = CHUNK_SIZE * 256; + +// -- Compressor trait --------------------------------------------------------- + +pub trait CompressorOp { + /// Feed plaintext in, append compressed bytes to `out`. + fn compress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError>; + + /// Signal EOF — flush remaining compressed bytes to `out`. + fn finish(&mut self, out: &mut Vec) -> Result<(), CryptoError>; +} + +pub trait DecompressorOp { + /// Feed compressed bytes in, append decompressed bytes to `out`. + fn decompress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError>; + + /// Signal EOF — flush remaining decompressed bytes to `out`. + fn finish(&mut self, out: &mut Vec) -> Result<(), CryptoError>; +} + +// -- Factory ------------------------------------------------------------------ + +pub fn new_compressor( + algo: CompressionAlgorithm, + level: Option, +) -> Result, CryptoError> { + match algo { + CompressionAlgorithm::Zstd => { + let level = level.unwrap_or(super::zstd_impl::DEFAULT_LEVEL); + super::zstd_impl::validate_level(level)?; + Ok(Box::new(ZstdCompressor::new(level)?)) + } + CompressionAlgorithm::Brotli => { + let level = level.unwrap_or(super::brotli_impl::DEFAULT_LEVEL as i32); + let level = u32::try_from(level).map_err(|_| { + CryptoError::InvalidParameter(format!( + "Brotli level must be non-negative, got {level}" + )) + })?; + super::brotli_impl::validate_level(level)?; + Ok(Box::new(BrotliCompressor::new(level))) + } + CompressionAlgorithm::None => Ok(Box::new(PassthroughCodec {})), + } +} + +pub fn new_decompressor( + algo: CompressionAlgorithm, +) -> Result, CryptoError> { + match algo { + CompressionAlgorithm::Zstd => Ok(Box::new(ZstdDecompressor::new()?)), + CompressionAlgorithm::Brotli => Ok(Box::new(BrotliDecompressor::new())), + CompressionAlgorithm::None => Ok(Box::new(PassthroughCodec {})), + } +} + +// -- Passthrough (CompressionAlgorithm::None) --------------------------------- + +struct PassthroughCodec {} + +impl CompressorOp for PassthroughCodec { + fn compress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError> { + out.extend_from_slice(input); + Ok(()) + } + fn finish(&mut self, _out: &mut Vec) -> Result<(), CryptoError> { + Ok(()) + } +} + +impl DecompressorOp for PassthroughCodec { + fn decompress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError> { + out.extend_from_slice(input); + Ok(()) + } + fn finish(&mut self, _out: &mut Vec) -> Result<(), CryptoError> { + Ok(()) + } +} + +// -- Zstd streaming ----------------------------------------------------------- + +struct ZstdCompressor<'a> { + encoder: zstd::stream::raw::Encoder<'a>, +} + +impl<'a> ZstdCompressor<'a> { + fn new(level: i32) -> Result { + let encoder = zstd::stream::raw::Encoder::new(level) + .map_err(|e| CryptoError::CompressionFailed(format!("Zstd encoder init: {e}")))?; + Ok(Self { encoder }) + } +} + +impl CompressorOp for ZstdCompressor<'_> { + fn compress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError> { + use zstd::stream::raw::Operation; + + let mut src = input; + loop { + let prev_len = out.len(); + out.resize(prev_len + zstd::zstd_safe::CCtx::out_size(), 0); + let status = self + .encoder + .run_on_buffers(src, &mut out[prev_len..]) + .map_err(|e| CryptoError::CompressionFailed(format!("Zstd compress: {e}")))?; + out.truncate(prev_len + status.bytes_written); + src = &src[status.bytes_read..]; + if src.is_empty() { + break; + } + } + Ok(()) + } + + fn finish(&mut self, out: &mut Vec) -> Result<(), CryptoError> { + use zstd::stream::raw::{Operation, OutBuffer}; + + loop { + let mut buf = vec![0u8; zstd::zstd_safe::CCtx::out_size()]; + let mut ob = OutBuffer::around(&mut buf); + let remaining = self + .encoder + .finish(&mut ob, true) + .map_err(|e| CryptoError::CompressionFailed(format!("Zstd finish: {e}")))?; + let written = ob.pos(); + out.extend_from_slice(&buf[..written]); + if remaining == 0 { + break; + } + } + Ok(()) + } +} + +struct ZstdDecompressor<'a> { + decoder: zstd::stream::raw::Decoder<'a>, +} + +impl<'a> ZstdDecompressor<'a> { + fn new() -> Result { + let decoder = zstd::stream::raw::Decoder::new() + .map_err(|e| CryptoError::CompressionFailed(format!("Zstd decoder init: {e}")))?; + Ok(Self { decoder }) + } +} + +impl DecompressorOp for ZstdDecompressor<'_> { + fn decompress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError> { + use zstd::stream::raw::Operation; + + let start_len = out.len(); + let mut src = input; + loop { + let prev_len = out.len(); + out.resize(prev_len + zstd::zstd_safe::DCtx::out_size(), 0); + let status = self + .decoder + .run_on_buffers(src, &mut out[prev_len..]) + .map_err(|e| CryptoError::CompressionFailed(format!("Zstd decompress: {e}")))?; + out.truncate(prev_len + status.bytes_written); + src = &src[status.bytes_read..]; + if out.len() - start_len > MAX_DECOMP_PER_CHUNK { + return Err(CryptoError::CompressionFailed( + "Decompression output exceeded safety limit".into(), + )); + } + if src.is_empty() { + break; + } + } + Ok(()) + } + + fn finish(&mut self, _out: &mut Vec) -> Result<(), CryptoError> { + Ok(()) + } +} + +// -- Brotli streaming --------------------------------------------------------- +// +// Brotli's CompressorWriter wraps a W and writes compressed bytes +// to it. We use Vec as the inner writer — `compress_chunk` writes input +// through the compressor, `finish` drops it to flush the final frame. + +struct BrotliCompressor { + // Option so we can take() in finish() to trigger Drop/flush + inner: Option>>, +} + +impl BrotliCompressor { + fn new(level: u32) -> Self { + let params = brotli::enc::BrotliEncoderParams { + quality: level as i32, + ..Default::default() + }; + let writer = + brotli::CompressorWriter::with_params(Vec::with_capacity(64 * 1024), 4096, ¶ms); + Self { + inner: Some(writer), + } + } +} + +impl CompressorOp for BrotliCompressor { + fn compress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError> { + let w = self.inner.as_mut().ok_or_else(|| { + CryptoError::CompressionFailed("Brotli compressor already finished".into()) + })?; + w.write_all(input) + .map_err(|e| CryptoError::CompressionFailed(format!("Brotli compress: {e}")))?; + // Brotli buffers internally — compressed bytes appear in the inner Vec + // once enough data accumulates or on flush/drop. + // Drain whatever compressed bytes are available so far. + let inner_vec = w.get_ref(); + if !inner_vec.is_empty() { + out.extend_from_slice(inner_vec); + w.get_mut().clear(); + } + Ok(()) + } + + fn finish(&mut self, out: &mut Vec) -> Result<(), CryptoError> { + let w = self.inner.take().ok_or_else(|| { + CryptoError::CompressionFailed("Brotli compressor already finished".into()) + })?; + // into_inner() flushes the brotli stream and returns the inner Vec + let compressed = w.into_inner(); + out.extend_from_slice(&compressed); + Ok(()) + } +} + +struct BrotliDecompressor { + // DecompressorWriter: write compressed bytes IN, decompressed bytes go to + // the inner Vec. Truly streaming — no whole-file buffering. + inner: Option>>, +} + +impl BrotliDecompressor { + fn new() -> Self { + Self { + inner: Some(brotli::DecompressorWriter::new( + Vec::with_capacity(64 * 1024), + 4096, + )), + } + } +} + +impl DecompressorOp for BrotliDecompressor { + fn decompress_chunk(&mut self, input: &[u8], out: &mut Vec) -> Result<(), CryptoError> { + let w = self.inner.as_mut().ok_or_else(|| { + CryptoError::CompressionFailed("Brotli decompressor already finished".into()) + })?; + let start_len = out.len(); + w.write_all(input) + .map_err(|e| CryptoError::CompressionFailed(format!("Brotli decompress: {e}")))?; + // Drain decompressed bytes that appeared in the inner Vec + let inner_vec = w.get_ref(); + if !inner_vec.is_empty() { + out.extend_from_slice(inner_vec); + w.get_mut().clear(); + } + if out.len() - start_len > MAX_DECOMP_PER_CHUNK { + return Err(CryptoError::CompressionFailed( + "Decompression output exceeded safety limit".into(), + )); + } + Ok(()) + } + + fn finish(&mut self, out: &mut Vec) -> Result<(), CryptoError> { + let w = self.inner.take().ok_or_else(|| { + CryptoError::CompressionFailed("Brotli decompressor already finished".into()) + })?; + // into_inner() flushes remaining decompressed data to the inner Vec + let remaining = w.into_inner().map_err(|_| { + CryptoError::CompressionFailed("Brotli decompressor flush failed".into()) + })?; + if !remaining.is_empty() { + out.extend_from_slice(&remaining); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_zstd_streaming_roundtrip() { + let input = b"Hello zstd streaming compression! ".repeat(1000); + let mut compressed = Vec::new(); + + let mut c = new_compressor(CompressionAlgorithm::Zstd, None).expect("zstd compressor"); + c.compress_chunk(&input[..500], &mut compressed).expect("compress chunk 1"); + c.compress_chunk(&input[500..], &mut compressed).expect("compress chunk 2"); + c.finish(&mut compressed).expect("finish compress"); + + let mut decompressed = Vec::new(); + let mut d = new_decompressor(CompressionAlgorithm::Zstd).expect("zstd decompressor"); + d.decompress_chunk(&compressed, &mut decompressed).expect("decompress"); + d.finish(&mut decompressed).expect("finish decompress"); + + assert_eq!(decompressed, input); + } + + #[test] + fn test_brotli_streaming_roundtrip() { + let input = b"Hello brotli streaming compression! ".repeat(1000); + let mut compressed = Vec::new(); + + let mut c = new_compressor(CompressionAlgorithm::Brotli, None).expect("brotli compressor"); + c.compress_chunk(&input[..500], &mut compressed).expect("compress chunk 1"); + c.compress_chunk(&input[500..], &mut compressed).expect("compress chunk 2"); + c.finish(&mut compressed).expect("finish compress"); + + let mut decompressed = Vec::new(); + let mut d = new_decompressor(CompressionAlgorithm::Brotli).expect("brotli decompressor"); + d.decompress_chunk(&compressed, &mut decompressed).expect("decompress"); + d.finish(&mut decompressed).expect("finish decompress"); + + assert_eq!(decompressed, input); + } + + #[test] + fn test_none_streaming_passthrough() { + let input = b"passthrough data"; + let mut out = Vec::new(); + + let mut c = new_compressor(CompressionAlgorithm::None, None).expect("none compressor"); + c.compress_chunk(input, &mut out).expect("compress"); + c.finish(&mut out).expect("finish compress"); + assert_eq!(out, input); + + let mut dec_out = Vec::new(); + let mut d = new_decompressor(CompressionAlgorithm::None).expect("none decompressor"); + d.decompress_chunk(&out, &mut dec_out).expect("decompress"); + d.finish(&mut dec_out).expect("finish decompress"); + assert_eq!(dec_out, input); + } + + #[test] + fn test_empty_input() { + for algo in [ + CompressionAlgorithm::Zstd, + CompressionAlgorithm::Brotli, + CompressionAlgorithm::None, + ] { + let mut compressed = Vec::new(); + let mut c = new_compressor(algo, None).expect("compressor"); + c.finish(&mut compressed).expect("finish compress"); + + let mut decompressed = Vec::new(); + let mut d = new_decompressor(algo).expect("decompressor"); + if !compressed.is_empty() { + d.decompress_chunk(&compressed, &mut decompressed).expect("decompress"); + } + d.finish(&mut decompressed).expect("finish decompress"); + assert!(decompressed.is_empty(), "algo={algo:?}"); + } + } +} diff --git a/rust/src/core/compression/zstd_impl.rs b/rust/src/core/compression/zstd_impl.rs new file mode 100644 index 0000000..35a1aa6 --- /dev/null +++ b/rust/src/core/compression/zstd_impl.rs @@ -0,0 +1,33 @@ +//! Zstd compression wrapper. + +use crate::core::error::CryptoError; + +pub const DEFAULT_LEVEL: i32 = 3; +// zstd treats level 0 as "use library default" — we reject it +// to force callers to choose explicitly or use None. +pub const MIN_LEVEL: i32 = 1; +pub const MAX_LEVEL: i32 = 22; + +pub fn validate_level(level: i32) -> Result<(), CryptoError> { + if !(MIN_LEVEL..=MAX_LEVEL).contains(&level) { + return Err(CryptoError::InvalidParameter(format!( + "Zstd level must be {MIN_LEVEL}–{MAX_LEVEL}, got {level}" + ))); + } + Ok(()) +} + +pub fn compress(data: &[u8], level: i32) -> Result, CryptoError> { + validate_level(level)?; + zstd::encode_all(std::io::Cursor::new(data), level) + .map_err(|e| CryptoError::CompressionFailed(e.to_string())) +} + +pub fn decompress(data: &[u8]) -> Result, CryptoError> { + // Find the exact size of the first compressed frame so trailing + // zero-padding (used in streaming chunks) is ignored. + let frame_size = zstd::zstd_safe::find_frame_compressed_size(data) + .map_err(|e| CryptoError::CompressionFailed(format!("Bad zstd frame: {e}")))?; + zstd::decode_all(std::io::Cursor::new(&data[..frame_size])) + .map_err(|e| CryptoError::CompressionFailed(e.to_string())) +} diff --git a/rust/src/core/error.rs b/rust/src/core/error.rs index 90b602c..3c1a3c4 100644 --- a/rust/src/core/error.rs +++ b/rust/src/core/error.rs @@ -29,8 +29,23 @@ pub enum CryptoError { #[error("Invalid parameter: {0}")] InvalidParameter(String), + #[error("Compression failed: {0}")] + CompressionFailed(String), + #[error("Authentication failed")] AuthenticationFailed, + + #[error("Vault full: need {needed} bytes, {available} available")] + VaultFull { needed: u64, available: u64 }, + + #[error("Vault locked by another process")] + VaultLocked, + + #[error("Segment not found: {0}")] + SegmentNotFound(String), + + #[error("Vault corrupted: {0}")] + VaultCorrupted(String), } impl From for CryptoError { diff --git a/rust/src/core/mod.rs b/rust/src/core/mod.rs index a314d3b..07318dd 100644 --- a/rust/src/core/mod.rs +++ b/rust/src/core/mod.rs @@ -3,8 +3,10 @@ //! This module contains the foundational types used throughout the crate. //! It is not exposed to FRB - only api/ modules are scanned for bindings. +pub mod compression; pub mod error; pub mod format; pub mod rng; pub mod secret; +pub mod streaming; pub mod traits; diff --git a/rust/src/core/streaming.rs b/rust/src/core/streaming.rs new file mode 100644 index 0000000..56a07d9 --- /dev/null +++ b/rust/src/core/streaming.rs @@ -0,0 +1,816 @@ +//! Low-level streaming primitives for chunk-based encryption. +//! +//! Defines the on-disk format: a 16-byte header followed by uniformly-sized +//! encrypted chunks. The header intentionally omits chunk count to prevent +//! metadata leakage, and the last chunk is padded to uniform size. + +use std::io::{Read, Write}; + +use crate::api::compression::CompressionAlgorithm; +use crate::core::error::CryptoError; +use crate::core::format::Algorithm; + +// -- Constants ---------------------------------------------------------------- + +/// Plaintext chunk size (64 KiB). +pub const CHUNK_SIZE: usize = 64 * 1024; + +/// AEAD nonce size (12 bytes for AES-GCM / ChaCha20-Poly1305). +pub const NONCE_SIZE: usize = 12; + +/// AEAD authentication tag size (16 bytes). +pub const TAG_SIZE: usize = 16; + +/// Every encrypted chunk on disk is exactly this size — no size leakage. +pub const ENCRYPTED_CHUNK_SIZE: usize = NONCE_SIZE + CHUNK_SIZE + TAG_SIZE; + +/// Stream header magic bytes. +pub const STREAM_MAGIC: &[u8; 4] = b"MSSE"; + +/// Stream format version. +pub const STREAM_VERSION: u16 = 1; + +/// Stream header size in bytes. +pub const STREAM_HEADER_SIZE: usize = 16; + +/// Per-chunk AAD size: index (8 bytes) + is_final (1 byte). +pub const AAD_SIZE: usize = 9; + +/// Length prefix size for last-chunk padding. +const PADDING_PREFIX_SIZE: usize = 4; + +// -- Algorithm ID (stream-specific) ------------------------------------------- + +/// Algorithm identifiers stored in the stream header. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum StreamAlgorithm { + AesGcm = 0x0001, + ChaCha20Poly1305 = 0x0002, +} + +impl StreamAlgorithm { + pub fn from_u16(v: u16) -> Result { + match v { + 0x0001 => Ok(Self::AesGcm), + 0x0002 => Ok(Self::ChaCha20Poly1305), + _ => Err(CryptoError::InvalidParameter(format!( + "Unknown stream algorithm: 0x{v:04X}" + ))), + } + } + + pub fn to_u16(self) -> u16 { + self as u16 + } +} + +impl From for Algorithm { + fn from(sa: StreamAlgorithm) -> Self { + match sa { + StreamAlgorithm::AesGcm => Algorithm::AesGcm, + StreamAlgorithm::ChaCha20Poly1305 => Algorithm::ChaCha20Poly1305, + } + } +} + +impl TryFrom for StreamAlgorithm { + type Error = CryptoError; + + fn try_from(a: Algorithm) -> Result { + match a { + Algorithm::AesGcm => Ok(StreamAlgorithm::AesGcm), + Algorithm::ChaCha20Poly1305 => Ok(StreamAlgorithm::ChaCha20Poly1305), + _ => Err(CryptoError::InvalidParameter(format!( + "Algorithm {a:?} not supported for streaming" + ))), + } + } +} + +// -- StreamHeader ------------------------------------------------------------- + +/// 16-byte header at the start of every encrypted stream. +/// +/// Chunk count is intentionally NOT stored — an observer must not learn +/// the file size from unencrypted metadata. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StreamHeader { + pub version: u16, + pub algorithm: StreamAlgorithm, + pub compression: CompressionAlgorithm, + // 7 reserved bytes (zeroed) — replaces chunk_count +} + +impl StreamHeader { + pub fn new(algorithm: StreamAlgorithm, compression: CompressionAlgorithm) -> Self { + Self { + version: STREAM_VERSION, + algorithm, + compression, + } + } + + pub fn to_bytes(&self) -> [u8; STREAM_HEADER_SIZE] { + let mut buf = [0u8; STREAM_HEADER_SIZE]; + buf[0..4].copy_from_slice(STREAM_MAGIC); + buf[4..6].copy_from_slice(&self.version.to_le_bytes()); + buf[6..8].copy_from_slice(&self.algorithm.to_u16().to_le_bytes()); + buf[8] = self.compression.to_u8(); + // bytes 9..16 stay zeroed (reserved) + buf + } + + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < STREAM_HEADER_SIZE { + return Err(CryptoError::InvalidParameter(format!( + "Stream header too short: {} bytes, need {STREAM_HEADER_SIZE}", + data.len() + ))); + } + + if &data[0..4] != STREAM_MAGIC { + return Err(CryptoError::InvalidParameter( + "Invalid stream magic bytes".to_string(), + )); + } + + let version = u16::from_le_bytes([data[4], data[5]]); + if version != STREAM_VERSION { + return Err(CryptoError::InvalidParameter(format!( + "Unsupported stream version: {version}" + ))); + } + + let algorithm = StreamAlgorithm::from_u16(u16::from_le_bytes([data[6], data[7]]))?; + + let compression = CompressionAlgorithm::from_u8(data[8])?; + + Ok(Self { + version, + algorithm, + compression, + }) + } +} + +// -- ChunkAad ----------------------------------------------------------------- + +/// Per-chunk authenticated additional data. +/// +/// Prevents reordering, duplication, truncation, and appending. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkAad { + pub index: u64, + pub is_final: bool, +} + +impl ChunkAad { + pub fn to_bytes(self) -> [u8; AAD_SIZE] { + let mut buf = [0u8; AAD_SIZE]; + buf[0..8].copy_from_slice(&self.index.to_le_bytes()); + buf[8] = u8::from(self.is_final); + buf + } + + pub fn from_bytes(bytes: &[u8; AAD_SIZE]) -> Self { + let index = u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + let is_final = bytes[8] != 0; + Self { index, is_final } + } +} + +// -- EncryptedChunk ----------------------------------------------------------- + +/// A single encrypted chunk as it appears on disk. +/// +/// Use `EncryptedChunk::new()` to pre-allocate, then reuse across reads +/// to avoid per-chunk heap allocation in hot loops. +pub struct EncryptedChunk { + pub nonce: [u8; NONCE_SIZE], + pub ciphertext: Vec, // always CHUNK_SIZE bytes + pub tag: [u8; TAG_SIZE], +} + +impl EncryptedChunk { + /// Pre-allocate a reusable chunk buffer. + pub fn new() -> Self { + Self { + nonce: [0u8; NONCE_SIZE], + ciphertext: vec![0u8; CHUNK_SIZE], + tag: [0u8; TAG_SIZE], + } + } +} + +impl Default for EncryptedChunk { + fn default() -> Self { + Self::new() + } +} + +// -- Last-chunk padding ------------------------------------------------------- + +/// Pad the last chunk's plaintext to exactly `CHUNK_SIZE`. +/// +/// Layout: `[real_len: u32 LE (4B)] [real_data] [zero padding]` +pub fn pad_last_chunk(data: &[u8]) -> Result, CryptoError> { + let real_len = data.len(); + let max_payload = CHUNK_SIZE - PADDING_PREFIX_SIZE; + + if real_len > max_payload { + return Err(CryptoError::InvalidParameter(format!( + "Last chunk payload too large: {real_len} bytes, max {max_payload}" + ))); + } + + let mut buf = vec![0u8; CHUNK_SIZE]; + buf[0..4].copy_from_slice(&(real_len as u32).to_le_bytes()); + buf[4..4 + real_len].copy_from_slice(data); + // remaining bytes stay zeroed + Ok(buf) +} + +/// Strip padding from a decrypted last chunk, returning only the real data. +/// +/// Validates that the length prefix is in range and that all padding +/// bytes beyond the real data are zero (rejects tampered padding). +pub fn strip_last_chunk_padding(padded: &[u8]) -> Result, CryptoError> { + if padded.len() != CHUNK_SIZE { + return Err(CryptoError::InvalidParameter(format!( + "Padded chunk wrong size: {} bytes, expected {CHUNK_SIZE}", + padded.len() + ))); + } + + let real_len = u32::from_le_bytes([padded[0], padded[1], padded[2], padded[3]]) as usize; + let max_payload = CHUNK_SIZE - PADDING_PREFIX_SIZE; + + if real_len > max_payload { + return Err(CryptoError::InvalidParameter(format!( + "Invalid padding length: {real_len}, max {max_payload}" + ))); + } + + // Reject non-zero bytes in padding region + let padding_start = PADDING_PREFIX_SIZE + real_len; + if !padded[padding_start..].iter().all(|&b| b == 0) { + return Err(CryptoError::InvalidParameter( + "Non-zero bytes in padding region".to_string(), + )); + } + + Ok(padded[PADDING_PREFIX_SIZE..PADDING_PREFIX_SIZE + real_len].to_vec()) +} + +// -- ChunkReader -------------------------------------------------------------- + +/// Reads encrypted chunks sequentially from a stream. +/// +/// Uses an internal buffer for atomic reads (one I/O call per chunk). +/// Caller provides a reusable `EncryptedChunk` via `read_chunk()` to +/// avoid per-chunk heap allocation. +pub struct ChunkReader { + reader: R, + buf: Vec, +} + +impl ChunkReader { + pub fn new(reader: R) -> Self { + Self { + reader, + buf: vec![0u8; ENCRYPTED_CHUNK_SIZE], + } + } + + /// Read and parse the 16-byte stream header. + pub fn read_header(&mut self) -> Result { + let mut header_buf = [0u8; STREAM_HEADER_SIZE]; + self.reader + .read_exact(&mut header_buf) + .map_err(|e| CryptoError::IoError(format!("Failed to read stream header: {e}")))?; + StreamHeader::from_bytes(&header_buf) + } + + /// Read the next encrypted chunk into `chunk`. Returns `true` if a + /// chunk was read, `false` at clean EOF. + /// + /// Reuse the same `EncryptedChunk` across iterations — zero heap + /// allocation in the hot loop. + pub fn read_chunk(&mut self, chunk: &mut EncryptedChunk) -> Result { + match read_exact_or_eof(&mut self.reader, &mut self.buf) { + Ok(true) => {} + Ok(false) => return Ok(false), + Err(e) => return Err(CryptoError::IoError(format!("Failed to read chunk: {e}"))), + } + + chunk.nonce.copy_from_slice(&self.buf[..NONCE_SIZE]); + chunk.ciphertext[..CHUNK_SIZE] + .copy_from_slice(&self.buf[NONCE_SIZE..NONCE_SIZE + CHUNK_SIZE]); + chunk + .tag + .copy_from_slice(&self.buf[NONCE_SIZE + CHUNK_SIZE..]); + + Ok(true) + } + + /// Consume the reader and return the underlying stream. + pub fn into_inner(self) -> R { + self.reader + } +} + +/// Read exactly `buf.len()` bytes, or detect clean EOF (0 bytes read). +/// Returns `Ok(true)` on full read, `Ok(false)` on clean EOF. +/// Returns `Err` on partial read (corrupt/truncated stream). +fn read_exact_or_eof(reader: &mut R, buf: &mut [u8]) -> Result { + let mut offset = 0; + loop { + match reader.read(&mut buf[offset..]) { + Ok(0) => { + if offset == 0 { + return Ok(false); // clean EOF + } + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + format!("Truncated chunk: read {offset} of {} bytes", buf.len()), + )); + } + Ok(n) => { + offset += n; + if offset == buf.len() { + return Ok(true); + } + } + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } +} + +// -- ChunkWriter -------------------------------------------------------------- + +/// Writes encrypted chunks sequentially to a stream. +/// +/// Uses an internal buffer to batch nonce+ciphertext+tag into a single +/// `write_all` call, reducing syscall overhead. +pub struct ChunkWriter { + writer: W, + buf: Vec, +} + +impl ChunkWriter { + pub fn new(writer: W) -> Self { + Self { + writer, + buf: vec![0u8; ENCRYPTED_CHUNK_SIZE], + } + } + + /// Write the 16-byte stream header. + pub fn write_header(&mut self, header: &StreamHeader) -> Result<(), CryptoError> { + self.writer + .write_all(&header.to_bytes()) + .map_err(|e| CryptoError::IoError(format!("Failed to write stream header: {e}"))) + } + + /// Write one encrypted chunk in a single I/O call. + /// + /// Caller must ensure ciphertext is exactly `CHUNK_SIZE` bytes. + pub fn write_chunk(&mut self, chunk: &EncryptedChunk) -> Result<(), CryptoError> { + if chunk.ciphertext.len() != CHUNK_SIZE { + return Err(CryptoError::InvalidParameter(format!( + "Chunk ciphertext must be {CHUNK_SIZE} bytes, got {}", + chunk.ciphertext.len() + ))); + } + + self.buf[..NONCE_SIZE].copy_from_slice(&chunk.nonce); + self.buf[NONCE_SIZE..NONCE_SIZE + CHUNK_SIZE].copy_from_slice(&chunk.ciphertext); + self.buf[NONCE_SIZE + CHUNK_SIZE..].copy_from_slice(&chunk.tag); + + self.writer + .write_all(&self.buf) + .map_err(|e| CryptoError::IoError(format!("Failed to write chunk: {e}"))) + } + + /// Flush and return the underlying writer. + pub fn finish(mut self) -> Result { + self.writer + .flush() + .map_err(|e| CryptoError::IoError(format!("Failed to flush: {e}")))?; + Ok(self.writer) + } +} + +/// Flush, fsync, and consume a file-backed writer to guarantee durability. +pub fn finish_file( + writer: ChunkWriter>, +) -> Result<(), CryptoError> { + let buf_writer = writer.finish()?; + buf_writer + .get_ref() + .sync_all() + .map_err(|e| CryptoError::IoError(format!("Failed to fsync: {e}"))) +} + +/// Compute the byte offset of chunk `k` within the stream (after the header). +pub fn chunk_offset(k: u64) -> u64 { + STREAM_HEADER_SIZE as u64 + k * ENCRYPTED_CHUNK_SIZE as u64 +} + +// -- Tests -------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_header_roundtrip() { + let header = StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None); + let bytes = header.to_bytes(); + + assert_eq!(bytes.len(), STREAM_HEADER_SIZE); + assert_eq!(&bytes[0..4], b"MSSE"); + // reserved bytes are zeroed + assert!(bytes[8..16].iter().all(|&b| b == 0)); + + let parsed = StreamHeader::from_bytes(&bytes).expect("parse failed"); + assert_eq!(header, parsed); + + // Also test ChaCha20 variant + let header2 = StreamHeader::new(StreamAlgorithm::ChaCha20Poly1305, CompressionAlgorithm::None); + let bytes2 = header2.to_bytes(); + let parsed2 = StreamHeader::from_bytes(&bytes2).expect("parse failed"); + assert_eq!(header2, parsed2); + } + + #[test] + fn test_single_chunk_roundtrip() { + let mut output = Vec::new(); + + { + let mut writer = ChunkWriter::new(&mut output); + let header = StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None); + writer.write_header(&header).expect("write header"); + + let chunk = EncryptedChunk { + nonce: [0xAA; NONCE_SIZE], + ciphertext: vec![0xBB; CHUNK_SIZE], + tag: [0xCC; TAG_SIZE], + }; + writer.write_chunk(&chunk).expect("write chunk"); + } + + assert_eq!(output.len(), STREAM_HEADER_SIZE + ENCRYPTED_CHUNK_SIZE); + + let mut reader = ChunkReader::new(Cursor::new(&output)); + let header = reader.read_header().expect("read header"); + assert_eq!(header.algorithm, StreamAlgorithm::AesGcm); + + let mut chunk = EncryptedChunk::new(); + assert!(reader.read_chunk(&mut chunk).expect("read chunk")); + assert_eq!(chunk.nonce, [0xAA; NONCE_SIZE]); + assert_eq!(chunk.ciphertext.len(), CHUNK_SIZE); + assert!(chunk.ciphertext.iter().all(|&b| b == 0xBB)); + assert_eq!(chunk.tag, [0xCC; TAG_SIZE]); + + assert!(!reader.read_chunk(&mut chunk).expect("eof")); + } + + #[test] + fn test_multi_chunk_roundtrip() { + let num_chunks = 5; + let mut output = Vec::new(); + + { + let mut writer = ChunkWriter::new(&mut output); + writer + .write_header(&StreamHeader::new(StreamAlgorithm::ChaCha20Poly1305, CompressionAlgorithm::None)) + .expect("write header"); + + for i in 0..num_chunks { + let chunk = EncryptedChunk { + nonce: [i as u8; NONCE_SIZE], + ciphertext: vec![i as u8 + 0x10; CHUNK_SIZE], + tag: [i as u8 + 0x20; TAG_SIZE], + }; + writer.write_chunk(&chunk).expect("write chunk"); + } + } + + let expected_size = STREAM_HEADER_SIZE + num_chunks * ENCRYPTED_CHUNK_SIZE; + assert_eq!(output.len(), expected_size); + + let mut reader = ChunkReader::new(Cursor::new(&output)); + let header = reader.read_header().expect("read header"); + assert_eq!(header.algorithm, StreamAlgorithm::ChaCha20Poly1305); + + // Reuse one chunk across all reads — zero allocation in hot loop + let mut chunk = EncryptedChunk::new(); + for i in 0..num_chunks { + assert!(reader.read_chunk(&mut chunk).expect("read chunk")); + assert_eq!(chunk.nonce, [i as u8; NONCE_SIZE]); + assert!(chunk.ciphertext.iter().all(|&b| b == i as u8 + 0x10)); + assert_eq!(chunk.tag, [i as u8 + 0x20; TAG_SIZE]); + } + + assert!(!reader.read_chunk(&mut chunk).expect("eof")); + } + + #[test] + fn test_bad_magic_rejected() { + let mut bytes = StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None).to_bytes(); + bytes[0] = b'X'; + let result = StreamHeader::from_bytes(&bytes); + assert!(result.is_err()); + let err = result.expect_err("should fail").to_string(); + assert!(err.contains("magic"), "Error should mention magic: {err}"); + } + + #[test] + fn test_chunk_offset_calculation() { + for k in 0..6u64 { + let expected = STREAM_HEADER_SIZE as u64 + k * ENCRYPTED_CHUNK_SIZE as u64; + assert_eq!(chunk_offset(k), expected, "offset mismatch for chunk {k}"); + } + } + + #[test] + fn test_aad_serialization() { + let aad = ChunkAad { + index: 42, + is_final: true, + }; + let bytes = aad.to_bytes(); + assert_eq!(bytes.len(), AAD_SIZE); + + // index = 42 in LE + let mut expected_index = [0u8; 8]; + expected_index[0] = 42; + assert_eq!(&bytes[0..8], &expected_index); + // is_final = true + assert_eq!(bytes[8], 1); + + let roundtrip = ChunkAad::from_bytes(&bytes); + assert_eq!(roundtrip, aad); + + // Also test is_final = false + let aad2 = ChunkAad { + index: 0, + is_final: false, + }; + let bytes2 = aad2.to_bytes(); + assert_eq!(bytes2[8], 0); + assert_eq!(ChunkAad::from_bytes(&bytes2), aad2); + } + + #[test] + fn test_all_chunks_uniform_size() { + let mut output = Vec::new(); + + { + let mut writer = ChunkWriter::new(&mut output); + writer + .write_header(&StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None)) + .expect("write header"); + + // Simulate 3 chunks — last one would be "short" in plaintext, + // but after padding the encrypted output is uniform. + for _ in 0..3 { + let chunk = EncryptedChunk { + nonce: [0u8; NONCE_SIZE], + ciphertext: vec![0u8; CHUNK_SIZE], + tag: [0u8; TAG_SIZE], + }; + writer.write_chunk(&chunk).expect("write chunk"); + } + } + + // Verify total size matches uniform chunk expectation + let data_portion = output.len() - STREAM_HEADER_SIZE; + assert_eq!(data_portion % ENCRYPTED_CHUNK_SIZE, 0); + assert_eq!(data_portion / ENCRYPTED_CHUNK_SIZE, 3); + } + + #[test] + fn test_last_chunk_padding_strip() { + let real_data = vec![0x42u8; 100]; + let padded = pad_last_chunk(&real_data).expect("pad"); + + assert_eq!(padded.len(), CHUNK_SIZE); + + // Length prefix + let stored_len = u32::from_le_bytes([padded[0], padded[1], padded[2], padded[3]]) as usize; + assert_eq!(stored_len, 100); + + // Real data follows prefix + assert_eq!(&padded[4..104], &real_data); + + // Padding is zeroes + assert!(padded[104..].iter().all(|&b| b == 0)); + + // Round-trip via strip + let stripped = strip_last_chunk_padding(&padded).expect("strip"); + assert_eq!(stripped, real_data); + } + + #[test] + fn test_padding_empty_payload() { + let padded = pad_last_chunk(&[]).expect("pad empty"); + assert_eq!(padded.len(), CHUNK_SIZE); + let stripped = strip_last_chunk_padding(&padded).expect("strip"); + assert!(stripped.is_empty()); + } + + #[test] + fn test_padding_max_payload() { + let max = CHUNK_SIZE - PADDING_PREFIX_SIZE; + let data = vec![0xFF; max]; + let padded = pad_last_chunk(&data).expect("pad max"); + assert_eq!(padded.len(), CHUNK_SIZE); + let stripped = strip_last_chunk_padding(&padded).expect("strip"); + assert_eq!(stripped.len(), max); + } + + #[test] + fn test_padding_overflow_rejected() { + let too_big = vec![0u8; CHUNK_SIZE]; // exceeds max payload + assert!(pad_last_chunk(&too_big).is_err()); + } + + #[test] + fn test_write_chunk_rejects_wrong_size() { + let mut output = Vec::new(); + let mut writer = ChunkWriter::new(&mut output); + + let bad_chunk = EncryptedChunk { + nonce: [0u8; NONCE_SIZE], + ciphertext: vec![0u8; 100], // wrong size + tag: [0u8; TAG_SIZE], + }; + + let result = writer.write_chunk(&bad_chunk); + assert!(result.is_err()); + } + + #[test] + fn test_truncated_stream_detected() { + let mut output = Vec::new(); + { + let mut writer = ChunkWriter::new(&mut output); + writer + .write_header(&StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None)) + .expect("write header"); + + let chunk = EncryptedChunk { + nonce: [0u8; NONCE_SIZE], + ciphertext: vec![0u8; CHUNK_SIZE], + tag: [0u8; TAG_SIZE], + }; + writer.write_chunk(&chunk).expect("write chunk"); + } + + // Truncate mid-chunk + output.truncate(STREAM_HEADER_SIZE + 100); + + let mut reader = ChunkReader::new(Cursor::new(&output)); + reader.read_header().expect("header ok"); + let mut chunk = EncryptedChunk::new(); + assert!( + reader.read_chunk(&mut chunk).is_err(), + "Should detect truncated chunk" + ); + } + + #[test] + fn test_header_too_short_rejected() { + let result = StreamHeader::from_bytes(&[0u8; 4]); + assert!(result.is_err()); + } + + #[test] + fn test_unsupported_version_rejected() { + let mut bytes = StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None).to_bytes(); + bytes[4] = 0xFF; // bad version (LE low byte) + bytes[5] = 0x00; + let result = StreamHeader::from_bytes(&bytes); + assert!(result.is_err()); + } + + #[test] + fn test_unknown_algorithm_rejected() { + let result = StreamAlgorithm::from_u16(0xFFFF); + assert!(result.is_err()); + } + + #[test] + fn test_tampered_padding_rejected() { + let real_data = vec![0x42u8; 100]; + let mut padded = pad_last_chunk(&real_data).expect("pad"); + + // Inject non-zero byte in the padding region + padded[CHUNK_SIZE - 1] = 0xFF; + + let result = strip_last_chunk_padding(&padded); + assert!(result.is_err()); + let err = result.expect_err("should fail").to_string(); + assert!( + err.contains("padding"), + "Error should mention padding: {err}" + ); + } + + #[test] + fn test_algorithm_conversion_roundtrip() { + let algo: Algorithm = StreamAlgorithm::AesGcm.into(); + assert_eq!(algo, Algorithm::AesGcm); + + let algo: Algorithm = StreamAlgorithm::ChaCha20Poly1305.into(); + assert_eq!(algo, Algorithm::ChaCha20Poly1305); + + let sa: StreamAlgorithm = Algorithm::AesGcm.try_into().expect("aes"); + assert_eq!(sa, StreamAlgorithm::AesGcm); + + let sa: StreamAlgorithm = Algorithm::ChaCha20Poly1305.try_into().expect("chacha"); + assert_eq!(sa, StreamAlgorithm::ChaCha20Poly1305); + + // XChaCha20 not supported for streaming yet + let result: Result = Algorithm::XChaCha20Poly1305.try_into(); + assert!(result.is_err()); + } + + #[test] + fn test_writer_finish_returns_inner() { + let output = Vec::new(); + let mut writer = ChunkWriter::new(output); + writer + .write_header(&StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None)) + .expect("write header"); + + let recovered = writer.finish().expect("finish"); + assert_eq!(recovered.len(), STREAM_HEADER_SIZE); + } + + #[test] + fn test_reader_into_inner() { + let data = StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None).to_bytes(); + let cursor = Cursor::new(data.to_vec()); + let mut reader = ChunkReader::new(cursor); + reader.read_header().expect("read header"); + + let inner = reader.into_inner(); + assert_eq!(inner.position(), STREAM_HEADER_SIZE as u64); + } + + #[test] + fn test_encrypted_chunk_default() { + let chunk = EncryptedChunk::default(); + assert_eq!(chunk.nonce, [0u8; NONCE_SIZE]); + assert_eq!(chunk.ciphertext.len(), CHUNK_SIZE); + assert_eq!(chunk.tag, [0u8; TAG_SIZE]); + } + + #[test] + fn test_chunk_reuse_overwrites_previous() { + let mut output = Vec::new(); + { + let mut writer = ChunkWriter::new(&mut output); + writer + .write_header(&StreamHeader::new(StreamAlgorithm::AesGcm, CompressionAlgorithm::None)) + .expect("header"); + + for i in 0u8..3 { + let chunk = EncryptedChunk { + nonce: [i + 1; NONCE_SIZE], + ciphertext: vec![i + 0x10; CHUNK_SIZE], + tag: [i + 0x20; TAG_SIZE], + }; + writer.write_chunk(&chunk).expect("write"); + } + } + + let mut reader = ChunkReader::new(Cursor::new(&output)); + reader.read_header().expect("header"); + + let mut chunk = EncryptedChunk::new(); + + assert!(reader.read_chunk(&mut chunk).expect("c0")); + assert_eq!(chunk.nonce[0], 1); + assert_eq!(chunk.ciphertext[0], 0x10); + + // Second read overwrites first chunk's data + assert!(reader.read_chunk(&mut chunk).expect("c1")); + assert_eq!(chunk.nonce[0], 2); + assert_eq!(chunk.ciphertext[0], 0x11); + + assert!(reader.read_chunk(&mut chunk).expect("c2")); + assert_eq!(chunk.nonce[0], 3); + assert_eq!(chunk.ciphertext[0], 0x12); + + assert!(!reader.read_chunk(&mut chunk).expect("eof")); + } +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 37dcbe7..4a70019 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -25,10 +25,8 @@ // Section: imports -use crate::api::encryption::noop::*; use crate::api::encryption::*; use crate::api::hashing::*; -use crate::core::traits::Encryption; use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; use flutter_rust_bridge::{Handler, IntoIntoDart}; @@ -41,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1451637143; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1664706909; // Section: executor @@ -49,7 +47,7 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs -fn wire__crate__api__encryption__noop__NoopEncryption_algorithm_id_impl( +fn wire__crate__api__hashing__argon2__argon2id_hash_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -57,7 +55,7 @@ fn wire__crate__api__encryption__noop__NoopEncryption_algorithm_id_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "NoopEncryption_algorithm_id", + debug_name: "argon2id_hash", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -71,38 +69,21 @@ fn wire__crate__api__encryption__noop__NoopEncryption_algorithm_id_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_that = , - >>::sse_decode(&mut deserializer); + let api_password = ::sse_decode(&mut deserializer); + let api_preset = + ::sse_decode(&mut deserializer); deserializer.end(); move |context| { - transform_result_sse::<_, ()>((move || { - let mut api_that_guard = None; - let decode_indices_ = - flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ - flutter_rust_bridge::for_generated::LockableOrderInfo::new( - &api_that, 0, false, - ), - ]); - for i in decode_indices_ { - match i { - 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), - _ => unreachable!(), - } - } - let api_that_guard = api_that_guard.unwrap(); - let output_ok = Result::<_, ()>::Ok({ - crate::api::encryption::noop::NoopEncryption::algorithm_id( - &*api_that_guard, - ); - })?; + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let output_ok = + crate::api::hashing::argon2::argon2id_hash(api_password, api_preset)?; Ok(output_ok) })()) } }, ) } -fn wire__crate__api__encryption__noop__NoopEncryption_decrypt_impl( +fn wire__crate__api__hashing__argon2__argon2id_hash_with_salt_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -110,7 +91,7 @@ fn wire__crate__api__encryption__noop__NoopEncryption_decrypt_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "NoopEncryption_decrypt", + debug_name: "argon2id_hash_with_salt", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -124,32 +105,17 @@ fn wire__crate__api__encryption__noop__NoopEncryption_decrypt_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_that = , - >>::sse_decode(&mut deserializer); - let api_ciphertext = >::sse_decode(&mut deserializer); - let api__aad = >::sse_decode(&mut deserializer); + let api_password = ::sse_decode(&mut deserializer); + let api_salt = ::sse_decode(&mut deserializer); + let api_preset = + ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, crate::core::error::CryptoError>((move || { - let mut api_that_guard = None; - let decode_indices_ = - flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ - flutter_rust_bridge::for_generated::LockableOrderInfo::new( - &api_that, 0, false, - ), - ]); - for i in decode_indices_ { - match i { - 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), - _ => unreachable!(), - } - } - let api_that_guard = api_that_guard.unwrap(); - let output_ok = crate::api::encryption::noop::NoopEncryption::decrypt( - &*api_that_guard, - &api_ciphertext, - &api__aad, + let output_ok = crate::api::hashing::argon2::argon2id_hash_with_salt( + api_password, + api_salt, + api_preset, )?; Ok(output_ok) })()) @@ -157,7 +123,7 @@ fn wire__crate__api__encryption__noop__NoopEncryption_decrypt_impl( }, ) } -fn wire__crate__api__encryption__noop__NoopEncryption_encrypt_impl( +fn wire__crate__api__hashing__argon2__argon2id_verify_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -165,7 +131,7 @@ fn wire__crate__api__encryption__noop__NoopEncryption_encrypt_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "NoopEncryption_encrypt", + debug_name: "argon2id_verify", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -179,40 +145,20 @@ fn wire__crate__api__encryption__noop__NoopEncryption_encrypt_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_that = , - >>::sse_decode(&mut deserializer); - let api_plaintext = >::sse_decode(&mut deserializer); - let api__aad = >::sse_decode(&mut deserializer); + let api_phc_hash = ::sse_decode(&mut deserializer); + let api_password = ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, crate::core::error::CryptoError>((move || { - let mut api_that_guard = None; - let decode_indices_ = - flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ - flutter_rust_bridge::for_generated::LockableOrderInfo::new( - &api_that, 0, false, - ), - ]); - for i in decode_indices_ { - match i { - 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), - _ => unreachable!(), - } - } - let api_that_guard = api_that_guard.unwrap(); - let output_ok = crate::api::encryption::noop::NoopEncryption::encrypt( - &*api_that_guard, - &api_plaintext, - &api__aad, - )?; + let output_ok = + crate::api::hashing::argon2::argon2id_verify(api_phc_hash, api_password)?; Ok(output_ok) })()) } }, ) } -fn wire__crate__api__hashing__argon2__argon2id_hash_impl( +fn wire__crate__api__hashing__blake3_hash_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -220,7 +166,7 @@ fn wire__crate__api__hashing__argon2__argon2id_hash_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "argon2id_hash", + debug_name: "blake3_hash", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -234,21 +180,19 @@ fn wire__crate__api__hashing__argon2__argon2id_hash_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_password = ::sse_decode(&mut deserializer); - let api_preset = - ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); deserializer.end(); move |context| { - transform_result_sse::<_, crate::core::error::CryptoError>((move || { + transform_result_sse::<_, ()>((move || { let output_ok = - crate::api::hashing::argon2::argon2id_hash(api_password, api_preset)?; + Result::<_, ()>::Ok(crate::api::hashing::blake3_hash(api_data))?; Ok(output_ok) })()) } }, ) } -fn wire__crate__api__hashing__argon2__argon2id_hash_with_salt_impl( +fn wire__crate__api__compression__compress_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -256,7 +200,7 @@ fn wire__crate__api__hashing__argon2__argon2id_hash_with_salt_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "argon2id_hash_with_salt", + debug_name: "compress", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -270,25 +214,20 @@ fn wire__crate__api__hashing__argon2__argon2id_hash_with_salt_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_password = ::sse_decode(&mut deserializer); - let api_salt = ::sse_decode(&mut deserializer); - let api_preset = - ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + let api_config = + ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, crate::core::error::CryptoError>((move || { - let output_ok = crate::api::hashing::argon2::argon2id_hash_with_salt( - api_password, - api_salt, - api_preset, - )?; + let output_ok = crate::api::compression::compress(&api_data, &api_config)?; Ok(output_ok) })()) } }, ) } -fn wire__crate__api__hashing__argon2__argon2id_verify_impl( +fn wire__crate__api__compression__compression_algorithm_from_u8_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -296,7 +235,7 @@ fn wire__crate__api__hashing__argon2__argon2id_verify_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "argon2id_verify", + debug_name: "compression_algorithm_from_u8", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -310,20 +249,19 @@ fn wire__crate__api__hashing__argon2__argon2id_verify_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_phc_hash = ::sse_decode(&mut deserializer); - let api_password = ::sse_decode(&mut deserializer); + let api_byte = ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, crate::core::error::CryptoError>((move || { let output_ok = - crate::api::hashing::argon2::argon2id_verify(api_phc_hash, api_password)?; + crate::api::compression::CompressionAlgorithm::from_u8(api_byte)?; Ok(output_ok) })()) } }, ) } -fn wire__crate__api__hashing__blake3_hash_impl( +fn wire__crate__api__compression__compression_algorithm_to_u8_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -331,7 +269,7 @@ fn wire__crate__api__hashing__blake3_hash_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "blake3_hash", + debug_name: "compression_algorithm_to_u8", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -345,12 +283,14 @@ fn wire__crate__api__hashing__blake3_hash_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_data = >::sse_decode(&mut deserializer); + let api_that = + ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::hashing::blake3_hash(api_data))?; + let output_ok = Result::<_, ()>::Ok( + crate::api::compression::CompressionAlgorithm::to_u8(api_that), + )?; Ok(output_ok) })()) } @@ -520,6 +460,41 @@ fn wire__crate__api__hashing__create_sha3_impl( }, ) } +fn wire__crate__api__compression__decompress_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "decompress", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_data = >::sse_decode(&mut deserializer); + let api_algorithm = + ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let output_ok = crate::api::compression::decompress(&api_data, api_algorithm)?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__crate__api__encryption__decrypt_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1159,63 +1134,394 @@ fn wire__crate__api__hashing__sha3_hash_impl( }, ) } - -// Section: related_funcs - -flutter_rust_bridge::frb_generated_moi_arc_impl_value!( - flutter_rust_bridge::for_generated::RustAutoOpaqueInner -); -flutter_rust_bridge::frb_generated_moi_arc_impl_value!( - flutter_rust_bridge::for_generated::RustAutoOpaqueInner -); -flutter_rust_bridge::frb_generated_moi_arc_impl_value!( - flutter_rust_bridge::for_generated::RustAutoOpaqueInner -); - -// Section: dart2rust - -impl SseDecode for CipherHandle { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = , - >>::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); - } -} - -impl SseDecode for HasherHandle { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = , - >>::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); - } -} - -impl SseDecode for NoopEncryption { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = , - >>::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); - } +fn wire__crate__api__compression__should_skip_compression_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "should_skip_compression", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_file_path = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::compression::should_skip_compression(&api_file_path), + )?; + Ok(output_ok) + })()) + } + }, + ) } - -impl SseDecode - for RustOpaqueMoi> -{ - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return decode_rust_opaque_moi(inner); - } +fn wire__crate__api__streaming__stream_compress_encrypt_file_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stream_compress_encrypt_file", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_cipher = , + >>::sse_decode(&mut deserializer); + let api_compression = + ::sse_decode(&mut deserializer); + let api_input_path = ::sse_decode(&mut deserializer); + let api_output_path = ::sse_decode(&mut deserializer); + let api_progress_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let mut api_cipher_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_cipher, + 0, + false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_cipher_guard = Some(api_cipher.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_cipher_guard = api_cipher_guard.unwrap(); + let output_ok = crate::api::streaming::stream_compress_encrypt_file( + &*api_cipher_guard, + api_compression, + api_input_path, + api_output_path, + api_progress_sink, + )?; + Ok(output_ok) + })()) + } + }, + ) } - +fn wire__crate__api__streaming__stream_decrypt_decompress_file_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stream_decrypt_decompress_file", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_cipher = , + >>::sse_decode(&mut deserializer); + let api_input_path = ::sse_decode(&mut deserializer); + let api_output_path = ::sse_decode(&mut deserializer); + let api_progress_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let mut api_cipher_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_cipher, + 0, + false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_cipher_guard = Some(api_cipher.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_cipher_guard = api_cipher_guard.unwrap(); + let output_ok = crate::api::streaming::stream_decrypt_decompress_file( + &*api_cipher_guard, + api_input_path, + api_output_path, + api_progress_sink, + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__streaming__stream_decrypt_file_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stream_decrypt_file", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_cipher = , + >>::sse_decode(&mut deserializer); + let api_input_path = ::sse_decode(&mut deserializer); + let api_output_path = ::sse_decode(&mut deserializer); + let api_progress_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let mut api_cipher_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_cipher, + 0, + false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_cipher_guard = Some(api_cipher.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_cipher_guard = api_cipher_guard.unwrap(); + let output_ok = crate::api::streaming::stream_decrypt_file( + &*api_cipher_guard, + api_input_path, + api_output_path, + api_progress_sink, + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__streaming__stream_encrypt_file_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stream_encrypt_file", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_cipher = , + >>::sse_decode(&mut deserializer); + let api_input_path = ::sse_decode(&mut deserializer); + let api_output_path = ::sse_decode(&mut deserializer); + let api_progress_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let mut api_cipher_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_cipher, + 0, + false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_cipher_guard = Some(api_cipher.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_cipher_guard = api_cipher_guard.unwrap(); + let output_ok = crate::api::streaming::stream_encrypt_file( + &*api_cipher_guard, + api_input_path, + api_output_path, + api_progress_sink, + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__streaming__stream_hash_file_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stream_hash_file", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_hasher = , + >>::sse_decode(&mut deserializer); + let api_file_path = ::sse_decode(&mut deserializer); + let api_progress_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::core::error::CryptoError>((move || { + let mut api_hasher_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_hasher, + 0, + false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_hasher_guard = Some(api_hasher.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_hasher_guard = api_hasher_guard.unwrap(); + let output_ok = crate::api::streaming::stream_hash_file( + &*api_hasher_guard, + api_file_path, + api_progress_sink, + )?; + Ok(output_ok) + })()) + } + }, + ) +} + +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); + } +} + +impl SseDecode for CipherHandle { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for HasherHandle { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + impl SseDecode - for RustOpaqueMoi> + for RustOpaqueMoi> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1225,7 +1531,7 @@ impl SseDecode } impl SseDecode - for RustOpaqueMoi> + for RustOpaqueMoi> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1234,6 +1540,14 @@ impl SseDecode } } +impl SseDecode for StreamSink { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return StreamSink::deserialize(inner); + } +} + impl SseDecode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1254,6 +1568,39 @@ impl SseDecode for crate::api::hashing::argon2::Argon2Preset { } } +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +impl SseDecode for crate::api::compression::CompressionAlgorithm { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::compression::CompressionAlgorithm::Zstd, + 1 => crate::api::compression::CompressionAlgorithm::Brotli, + 2 => crate::api::compression::CompressionAlgorithm::None, + _ => unreachable!("Invalid variant for CompressionAlgorithm: {}", inner), + }; + } +} + +impl SseDecode for crate::api::compression::CompressionConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_algorithm = + ::sse_decode(deserializer); + let mut var_level = >::sse_decode(deserializer); + return crate::api::compression::CompressionConfig { + algorithm: var_algorithm, + level: var_level, + }; + } +} + impl SseDecode for crate::core::error::CryptoError { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1294,6 +1641,10 @@ impl SseDecode for crate::core::error::CryptoError { return crate::core::error::CryptoError::InvalidParameter(var_field0); } 8 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::core::error::CryptoError::CompressionFailed(var_field0); + } + 9 => { return crate::core::error::CryptoError::AuthenticationFailed; } _ => { @@ -1303,6 +1654,13 @@ impl SseDecode for crate::core::error::CryptoError { } } +impl SseDecode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_f64::().unwrap() + } +} + impl SseDecode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1322,6 +1680,17 @@ impl SseDecode for Vec { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1352,13 +1721,6 @@ impl SseDecode for usize { } } -impl SseDecode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() != 0 - } -} - fn pde_ffi_dispatcher_primary_impl( func_id: i32, port: flutter_rust_bridge::for_generated::MessagePort, @@ -1368,40 +1730,35 @@ fn pde_ffi_dispatcher_primary_impl( ) { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 1 => wire__crate__api__encryption__noop__NoopEncryption_algorithm_id_impl( - port, - ptr, - rust_vec_len, - data_len, - ), - 2 => wire__crate__api__encryption__noop__NoopEncryption_decrypt_impl( + 1 => { + wire__crate__api__hashing__argon2__argon2id_hash_impl(port, ptr, rust_vec_len, data_len) + } + 2 => wire__crate__api__hashing__argon2__argon2id_hash_with_salt_impl( port, ptr, rust_vec_len, data_len, ), - 3 => wire__crate__api__encryption__noop__NoopEncryption_encrypt_impl( + 3 => wire__crate__api__hashing__argon2__argon2id_verify_impl( port, ptr, rust_vec_len, data_len, ), - 4 => { - wire__crate__api__hashing__argon2__argon2id_hash_impl(port, ptr, rust_vec_len, data_len) - } - 5 => wire__crate__api__hashing__argon2__argon2id_hash_with_salt_impl( + 4 => wire__crate__api__hashing__blake3_hash_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__compression__compress_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__compression__compression_algorithm_from_u8_impl( port, ptr, rust_vec_len, data_len, ), - 6 => wire__crate__api__hashing__argon2__argon2id_verify_impl( + 7 => wire__crate__api__compression__compression_algorithm_to_u8_impl( port, ptr, rust_vec_len, data_len, ), - 7 => wire__crate__api__hashing__blake3_hash_impl(port, ptr, rust_vec_len, data_len), 8 => { wire__crate__api__encryption__create_aes256_gcm_impl(port, ptr, rust_vec_len, data_len) } @@ -1419,46 +1776,72 @@ fn pde_ffi_dispatcher_primary_impl( data_len, ), 12 => wire__crate__api__hashing__create_sha3_impl(port, ptr, rust_vec_len, data_len), - 13 => wire__crate__api__encryption__decrypt_impl(port, ptr, rust_vec_len, data_len), - 14 => wire__crate__api__encryption__encrypt_impl(port, ptr, rust_vec_len, data_len), - 15 => wire__crate__api__encryption__encryption_algorithm_id_impl( + 13 => wire__crate__api__compression__decompress_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__api__encryption__decrypt_impl(port, ptr, rust_vec_len, data_len), + 15 => wire__crate__api__encryption__encrypt_impl(port, ptr, rust_vec_len, data_len), + 16 => wire__crate__api__encryption__encryption_algorithm_id_impl( port, ptr, rust_vec_len, data_len, ), - 16 => wire__crate__api__encryption__generate_aes256_gcm_key_impl( + 17 => wire__crate__api__encryption__generate_aes256_gcm_key_impl( port, ptr, rust_vec_len, data_len, ), - 17 => wire__crate__api__encryption__aes_gcm__generate_aes_key_impl( + 18 => wire__crate__api__encryption__aes_gcm__generate_aes_key_impl( port, ptr, rust_vec_len, data_len, ), - 18 => wire__crate__api__encryption__generate_chacha20_poly1305_key_impl( + 19 => wire__crate__api__encryption__generate_chacha20_poly1305_key_impl( port, ptr, rust_vec_len, data_len, ), - 19 => wire__crate__api__encryption__chacha20__generate_chacha_key_impl( + 20 => wire__crate__api__encryption__chacha20__generate_chacha_key_impl( port, ptr, rust_vec_len, data_len, ), - 20 => { + 21 => { wire__crate__api__hashing__hasher_algorithm_id_impl(port, ptr, rust_vec_len, data_len) } - 21 => wire__crate__api__hashing__hasher_finalize_impl(port, ptr, rust_vec_len, data_len), - 22 => wire__crate__api__hashing__hasher_reset_impl(port, ptr, rust_vec_len, data_len), - 23 => wire__crate__api__hashing__hasher_update_impl(port, ptr, rust_vec_len, data_len), - 25 => wire__crate__api__kdf__hkdf__hkdf_expand_impl(port, ptr, rust_vec_len, data_len), - 27 => wire__crate__api__hashing__sha3_hash_impl(port, ptr, rust_vec_len, data_len), + 22 => wire__crate__api__hashing__hasher_finalize_impl(port, ptr, rust_vec_len, data_len), + 23 => wire__crate__api__hashing__hasher_reset_impl(port, ptr, rust_vec_len, data_len), + 24 => wire__crate__api__hashing__hasher_update_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__kdf__hkdf__hkdf_expand_impl(port, ptr, rust_vec_len, data_len), + 28 => wire__crate__api__hashing__sha3_hash_impl(port, ptr, rust_vec_len, data_len), + 29 => wire__crate__api__compression__should_skip_compression_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 30 => wire__crate__api__streaming__stream_compress_encrypt_file_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 31 => wire__crate__api__streaming__stream_decrypt_decompress_file_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 32 => { + wire__crate__api__streaming__stream_decrypt_file_impl(port, ptr, rust_vec_len, data_len) + } + 33 => { + wire__crate__api__streaming__stream_encrypt_file_impl(port, ptr, rust_vec_len, data_len) + } + 34 => wire__crate__api__streaming__stream_hash_file_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1471,8 +1854,8 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 24 => wire__crate__api__kdf__hkdf__hkdf_derive_impl(ptr, rust_vec_len, data_len), - 26 => wire__crate__api__kdf__hkdf__hkdf_extract_impl(ptr, rust_vec_len, data_len), + 25 => wire__crate__api__kdf__hkdf__hkdf_derive_impl(ptr, rust_vec_len, data_len), + 27 => wire__crate__api__kdf__hkdf__hkdf_extract_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1509,21 +1892,6 @@ impl flutter_rust_bridge::IntoIntoDart> for HasherHandl } } -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for FrbWrapper { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} - -impl flutter_rust_bridge::IntoIntoDart> for NoopEncryption { - fn into_into_dart(self) -> FrbWrapper { - self.into() - } -} - // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::hashing::argon2::Argon2Preset { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -1546,6 +1914,49 @@ impl flutter_rust_bridge::IntoIntoDart flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Zstd => 0.into_dart(), + Self::Brotli => 1.into_dart(), + Self::None => 2.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::compression::CompressionAlgorithm +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::compression::CompressionAlgorithm +{ + fn into_into_dart(self) -> crate::api::compression::CompressionAlgorithm { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::compression::CompressionConfig { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.algorithm.into_into_dart().into_dart(), + self.level.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::compression::CompressionConfig +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::compression::CompressionConfig +{ + fn into_into_dart(self) -> crate::api::compression::CompressionConfig { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::core::error::CryptoError { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -1572,7 +1983,10 @@ impl flutter_rust_bridge::IntoDart for crate::core::error::CryptoError { crate::core::error::CryptoError::InvalidParameter(field0) => { [7.into_dart(), field0.into_into_dart().into_dart()].into_dart() } - crate::core::error::CryptoError::AuthenticationFailed => [8.into_dart()].into_dart(), + crate::core::error::CryptoError::CompressionFailed(field0) => { + [8.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::core::error::CryptoError::AuthenticationFailed => [9.into_dart()].into_dart(), _ => { unimplemented!(""); } @@ -1591,24 +2005,24 @@ impl flutter_rust_bridge::IntoIntoDart } } -impl SseEncode for CipherHandle { +impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + ::sse_encode(format!("{:?}", self), serializer); } } -impl SseEncode for HasherHandle { +impl SseEncode for CipherHandle { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); } } -impl SseEncode for NoopEncryption { +impl SseEncode for HasherHandle { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); } } @@ -1634,14 +2048,10 @@ impl SseEncode } } -impl SseEncode - for RustOpaqueMoi> -{ +impl SseEncode for StreamSink { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - let (ptr, size) = self.sse_encode_raw(); - ::sse_encode(ptr, serializer); - ::sse_encode(size, serializer); + unimplemented!("") } } @@ -1668,6 +2078,38 @@ impl SseEncode for crate::api::hashing::argon2::Argon2Preset { } } +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +impl SseEncode for crate::api::compression::CompressionAlgorithm { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::compression::CompressionAlgorithm::Zstd => 0, + crate::api::compression::CompressionAlgorithm::Brotli => 1, + crate::api::compression::CompressionAlgorithm::None => 2, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::api::compression::CompressionConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.algorithm, serializer); + >::sse_encode(self.level, serializer); + } +} + impl SseEncode for crate::core::error::CryptoError { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1703,8 +2145,12 @@ impl SseEncode for crate::core::error::CryptoError { ::sse_encode(7, serializer); ::sse_encode(field0, serializer); } - crate::core::error::CryptoError::AuthenticationFailed => { + crate::core::error::CryptoError::CompressionFailed(field0) => { ::sse_encode(8, serializer); + ::sse_encode(field0, serializer); + } + crate::core::error::CryptoError::AuthenticationFailed => { + ::sse_encode(9, serializer); } _ => { unimplemented!(""); @@ -1713,6 +2159,13 @@ impl SseEncode for crate::core::error::CryptoError { } } +impl SseEncode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_f64::(self).unwrap(); + } +} + impl SseEncode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1730,6 +2183,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1762,13 +2225,6 @@ impl SseEncode for usize { } } -impl SseEncode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self as _).unwrap(); - } -} - #[cfg(not(target_family = "wasm"))] mod io { // This file is automatically generated, so please do not edit it. @@ -1777,10 +2233,8 @@ mod io { // Section: imports use super::*; - use crate::api::encryption::noop::*; use crate::api::encryption::*; use crate::api::hashing::*; - use crate::core::traits::Encryption; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; @@ -1818,20 +2272,6 @@ mod io { ) { MoiArc::>::decrement_strong_count(ptr as _); } - - #[unsafe(no_mangle)] - pub extern "C" fn frbgen_m_security_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr: *const std::ffi::c_void, - ) { - MoiArc::>::increment_strong_count(ptr as _); - } - - #[unsafe(no_mangle)] - pub extern "C" fn frbgen_m_security_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr: *const std::ffi::c_void, - ) { - MoiArc::>::decrement_strong_count(ptr as _); - } } #[cfg(not(target_family = "wasm"))] pub use io::*; @@ -1845,10 +2285,8 @@ mod web { // Section: imports use super::*; - use crate::api::encryption::noop::*; use crate::api::encryption::*; use crate::api::hashing::*; - use crate::core::traits::Encryption; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; @@ -1888,20 +2326,6 @@ mod web { ) { MoiArc::>::decrement_strong_count(ptr as _); } - - #[wasm_bindgen] - pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr: *const std::ffi::c_void, - ) { - MoiArc::>::increment_strong_count(ptr as _); - } - - #[wasm_bindgen] - pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNoopEncryption( - ptr: *const std::ffi::c_void, - ) { - MoiArc::>::decrement_strong_count(ptr as _); - } } #[cfg(target_family = "wasm")] pub use web::*;