Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active April 2, 2023 12:28
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save PlugFox/b9c2b0c499238b3a6fb04793bb86fb44 to your computer and use it in GitHub Desktop.
Dart & Flutter AES Encryption
import 'dart:async';
import 'dart:math' as math;
import 'dart:typed_data' as td;
import 'package:meta/meta.dart';
/// {@template chunker}
/// Chunker stream transformer
/// {@endtemplate}
@immutable
class Chunker extends StreamTransformerBase<List<int>, td.Uint8List> {
/// {@macro chunker}
const Chunker(this.chunkSize);
/// Chunk size
final int chunkSize;
@override
Stream<td.Uint8List> bind(Stream<List<int>> stream) {
final controller = stream.isBroadcast
? StreamController<td.Uint8List>.broadcast(sync: true)
: StreamController<td.Uint8List>(sync: true);
return (controller..onListen = () => _onListen(stream, controller)).stream;
}
void _onListen(
Stream<List<int>> stream,
StreamController<td.Uint8List> controller,
) {
final sink = controller.sink;
final subscription = stream.listen(null, cancelOnError: false);
controller.onCancel = subscription.cancel;
if (!stream.isBroadcast) {
controller
..onPause = subscription.pause
..onResume = subscription.resume;
}
final bytes = td.BytesBuilder();
final onData =
_$onData(bytes, sink, subscription.pause, subscription.resume);
subscription
..onData(onData)
..onError(sink.addError)
..onDone(() {
if (bytes.isNotEmpty) sink.add(bytes.takeBytes());
sink.close();
});
}
void Function(List<int> data) _$onData(
td.BytesBuilder bytes,
StreamSink<td.Uint8List> sink,
void Function([Future<void>? resumeSignal]) pause,
void Function() resume,
) =>
(List<int> data) {
try {
final dataLength = data.length;
for (var offset = 0; offset < data.length; offset += chunkSize) {
var end = math.min<int>(offset + chunkSize, dataLength);
var to = math.min<int>(end, offset + chunkSize - bytes.length);
bytes.add(data.sublist(offset, to));
if (to != end) {
sink.add(bytes.takeBytes());
bytes.add(data.sublist(to, end));
}
if (bytes.length == chunkSize) {
sink.add(bytes.takeBytes());
}
}
} on Object catch (error, stackTrace) {
sink.addError(error, stackTrace);
}
};
}
/// Chunker extension methods.
/// sourceStream.Chunker<T>()
extension ChunkerX on Stream<List<int>> {
/// {@macro chunker}
Stream<td.Uint8List> chunker(int size) => transform(Chunker(size));
}
import 'dart:io' as io;
import 'package:cryptography/cryptography.dart' as cryptography;
//import 'package:cryptography_flutter/cryptography_flutter.dart' as cryptography;
import 'chunker.dart';
import 'package:path/path.dart' as path;
// --- Constants --- //
/// Buffer size in bytes.
const int _$buffer = 1024 * 1024; // 1 MB
/// Initialization vector (iv, salt, nonce) length in bytes.
const int _$nonceLength = 16;
/// Message authentication code (mac, tag, etc) length in bytes.
const int _$macLength = 16;
// Timing in ms on 25 mb file:
// sync ms: 4662
// 1 MB buffer: 3642
// 65'536 Byte buffer: 3618
// 8'192 Byte buffer: 3728
// Timing in ms on 218 mb file:
// sync ms: 41320
// 1 MB buffer: 30237
// 65'536 Byte buffer: 30630
// 8'192 Byte buffer: 31553
// --- Files --- //
const String sourcePath = '/Users/pfx/git/dartpad/dartpad/assets/text.txt';
//const String sourcePath = '/Users/pfx/git/dartpad/dartpad/assets/image.png';
//const String sourcePath = '/Users/pfx/git/dartpad/dartpad/assets/animation.gif';
//const String sourcePath = '/Users/pfx/git/dartpad/dartpad/assets/comics.cbz';
final io.File sourceFile = io.File(sourcePath);
final io.File tempFile = io.File(
'${path.withoutExtension(sourcePath)}'
'.tmp'
'${path.extension(sourcePath)}',
);
final io.File encryptedFile = io.File('$sourcePath.enc');
final io.File decryptedFile = io.File('${path.withoutExtension(sourcePath)}'
'.dec'
'${path.extension(sourcePath)}');
void main(List<String> arguments) => Future(() async {
const key = 'abcdef1234567890'; // 128, 192, 256 bits
final stopwatch = Stopwatch()..start();
await prepare();
await encrypt(key);
await decrypt(key);
print('Elapsed: ${stopwatch.elapsedMilliseconds} ms');
stopwatch.stop();
print(sourceFile.lengthSync() == decryptedFile.lengthSync()
? 'OK'
: 'FAIL');
});
Future<void> prepare() => Stream<io.File>.fromIterable(<io.File>[
tempFile,
encryptedFile,
decryptedFile,
]).where((f) => f.existsSync()).forEach((f) => f.delete());
Future<void> encrypt(String key) async {
assert([128, 192, 256].contains(key.length * 8));
assert(128 == _$nonceLength * 8);
assert(128 == _$macLength * 8);
assert(sourceFile.existsSync());
// --- Init algorithm & secret key --- //
final algorithm = cryptography.AesGcm.with128bits(nonceLength: _$nonceLength);
final secretKey = await algorithm.newSecretKeyFromBytes(key.codeUnits);
// --- Write to temp file --- //
final sink = tempFile.openWrite(mode: io.FileMode.writeOnly);
await sourceFile
.openRead()
.chunker(_$buffer)
.asyncMap<cryptography.SecretBox>((bytes) => algorithm.encrypt(
bytes,
secretKey: secretKey,
nonce: algorithm.newNonce(),
))
.expand<List<int>>((element) sync* {
yield element.nonce;
yield element.cipherText;
yield element.mac.bytes;
}).forEach(sink.add);
// --- Flush to temp file --- //
await sink.flush();
await sink.close();
// --- Move temp file to encrypted file --- //
await tempFile.rename(encryptedFile.path);
}
Future<void> decrypt(String key) async {
assert([128, 192, 256].contains(key.length * 8));
assert(128 == _$nonceLength * 8);
assert(128 == _$macLength * 8);
assert(encryptedFile.existsSync());
final algorithm = cryptography.AesGcm.with128bits(nonceLength: _$nonceLength);
final secretKey = await algorithm.newSecretKeyFromBytes(key.codeUnits);
final sink = tempFile.openWrite(mode: io.FileMode.writeOnly);
await encryptedFile
.openRead()
.chunker(_$nonceLength + _$buffer + _$macLength)
.asyncMap<cryptography.SecretBox>(
(bytes) => cryptography.SecretBox.fromConcatenation(
bytes,
nonceLength: _$nonceLength,
macLength: _$macLength,
))
.asyncMap<List<int>>((box) => algorithm.decrypt(
box,
secretKey: secretKey,
))
.forEach(sink.add);
await sink.flush();
await sink.close();
await tempFile.rename(decryptedFile.path);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment