For state management, I pick simple ChangeNotifier
, because it's enough for this app.
The app is designed to be extensible:
- To add a new encryption algorithm, you need to add a value in only one place:
lib/src/feature/encryption/data/encryption_algorithm.dart
, just add a new Enum value. - Encryption of files is done in parts, breaking them into pieces of a certain size, so the work of file encryption can be divided into several isolates.
- Encrypted chunks can be saved or transferred both in parts and as a single file.
You can find instructions about how to encode and decode below.
Simple encryption of file using AES-128-GCM with a 128-bit key, 128-bit nonce, and 128-bit MAC
import 'dart:io' as io;
import 'package:cryptography/cryptography.dart' as crypto;
import 'package:path/path.dart' as path;
const _$nonceLength = 16; // initialization vector, iv, salt, nonce, etc.
const _$macLength = 16; // message authentication code, mac, tag, etc.
Future<void> encrypt(io.File source, io.File encrypted, String key) async {
assert([128, 192, 256].contains(key.length * 8), 'Key length must be 128, 192, or 256 bits');
assert(128 == _$nonceLength * 8, 'Nonce length should be 128 bits');
assert(128 == _$macLength * 8, 'MAC length must be 128 bits');
assert(source.existsSync(), 'File does not exist');
final algorithm = crypto.AesGcm.with128bits(nonceLength: _$nonceLength);
final secretKey = await algorithm.newSecretKeyFromBytes(key.codeUnits);
final message = source.readAsBytesSync();
final secretBox = await algorithm.encrypt(
message,
secretKey: secretKey,
nonce: algorithm.newNonce(),
);
final sink = encrypted.openWrite(mode: io.FileMode.writeOnly)
..add(secretBox.nonce)
..add(secretBox.cipherText)
..add(secretBox.mac.bytes);
await sink.flush();
await sink.close();
}
Simple decryption of a file using AES-128-GCM with a 128-bit key, 128-bit nonce, and 128-bit MAC
import 'dart:io' as io;
import 'package:cryptography/cryptography.dart' as crypto;
import 'package:path/path.dart' as path;
const _$nonceLength = 16; // initialization vector, iv, salt, nonce, etc.
const _$macLength = 16; // message authentication code, mac, tag, etc.
Future<void> decrypt(io.File encrypted, io.File decrypted, String key) async {
assert([128, 192, 256].contains(key.length * 8), 'Key length must be 128, 192, or 256 bits');
assert(128 == _$nonceLength * 8, 'Nonce length should be 128 bits');
assert(128 == _$macLength * 8, 'MAC length must be 128 bits');
assert(encrypted.existsSync(), 'File does not exist');
final algorithm = crypto.AesGcm.with128bits(nonceLength: _$nonceLength);
final secretBox = crypto.SecretBox.fromConcatenation(
source.readAsBytesSync(),
nonceLength: _$nonceLength,
macLength: _$macLength,
);
final bytes = await algorithm.decrypt(
secretBox,
secretKey: await algorithm.newSecretKeyFromBytes(key.codeUnits),
);
decrypted.writeAsBytesSync(bytes);
}
Splitting into chunks will allow us to encrypt even very large files in several threads or isolates. For example, we can divide the load among the processor cores, where each isolate will be responsible for its file chunk offset. We can also store and transfer encrypted chunks as a single file or split them into parts.
Encryption:
- Set constants for Chunk size, Nonce (IV) length, and MAC length
- Get the secret key
- Prepare a temporary file for encrypted data into %TEMP% directory
- The open source file for reading as a Uint8List Stream
- Split the bytes stream into chunks of size
chunkSize
- Encrypt each chunk and write to a temporary file, adding the nonce to the beginning of each chunk and the MAC to the end of each chunk
- Close the temporary file
- Rename the temporary file to the encrypted file
- Call output callback with the encrypted file
Decryption:
- Set constants for Chunk size, Nonce (IV) length, and MAC length
- Get the secret key
- Prepare a temporary file for decrypted data into %TEMP% directory
- The open encrypted file for reading as a Uint8List Stream
- Split the bytes stream into chunks of size
nonceLength
+chunkSize
+macLength
- Decrypt each chunk and write to a temporary file
- Close the temporary file
- Rename the temporary file to the decrypted file
- Call output callback with the decrypted file