Last active
April 2, 2023 12:28
Star
You must be signed in to star a gist
Dart & Flutter AES Encryption
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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