diff --git a/lib/utils/crypto/aes.dart b/lib/utils/crypto/aes.dart new file mode 100644 index 0000000000..6dc0f0ef8a --- /dev/null +++ b/lib/utils/crypto/aes.dart @@ -0,0 +1,159 @@ +import 'dart:typed_data'; + +import 'package:common_crypto/common_crypto.dart' as cc; +import 'package:flutter/cupertino.dart'; +import 'package:pointycastle/block/aes.dart'; +import 'package:pointycastle/block/modes/cbc.dart'; +import 'package:pointycastle/padded_block_cipher/padded_block_cipher_impl.dart'; +import 'package:pointycastle/paddings/pkcs7.dart'; +import 'package:pointycastle/pointycastle.dart'; + +import '../platform.dart'; + +typedef OnCipherCallback = void Function(Uint8List data); + +abstract class AesCipher { + factory AesCipher({ + required Uint8List key, + required Uint8List iv, + required bool encrypt, + }) { + if (kPlatformIsDarwin) { + return AesCipherCommonCryptoImpl(key: key, iv: iv, encrypt: encrypt); + } else { + return AesCipherPointyCastleImpl(key: key, iv: iv, encrypt: encrypt); + } + } + + static Uint8List _crypt({ + required Uint8List key, + required Uint8List iv, + required Uint8List data, + required bool encrypt, + }) { + final cipher = AesCipher(key: key, iv: iv, encrypt: encrypt); + final result = []; + cipher + ..update(data, result.addAll) + ..finish(result.addAll); + return Uint8List.fromList(result); + } + + static Uint8List encrypt({ + required Uint8List key, + required Uint8List iv, + required Uint8List data, + }) => + _crypt(key: key, iv: iv, data: data, encrypt: true); + + static Uint8List decrypt({ + required Uint8List key, + required Uint8List iv, + required Uint8List data, + }) => + _crypt(key: key, iv: iv, data: data, encrypt: false); + + void update(Uint8List data, OnCipherCallback onCipher); + + void finish(OnCipherCallback onCipher); +} + +@visibleForTesting +class AesCipherCommonCryptoImpl implements AesCipher { + AesCipherCommonCryptoImpl({ + required Uint8List key, + required Uint8List iv, + required bool encrypt, + }) : _aesCrypto = cc.AesCryptor(key: key, iv: iv, encrypt: encrypt); + + final cc.AesCryptor _aesCrypto; + + var _disposed = false; + + @override + void update(Uint8List data, OnCipherCallback onCipher) { + if (_disposed) { + throw StateError('AesCipherCommonCryptoImpl has been disposed.'); + } + _aesCrypto.update(data, onCipher); + } + + @override + void finish(OnCipherCallback onCipher) { + if (_disposed) { + throw StateError('AesCipherCommonCryptoImpl has been disposed.'); + } + _aesCrypto + ..finalize(onCipher) + ..dispose(); + _disposed = true; + } +} + +class AesCipherPointyCastleImpl implements AesCipher { + AesCipherPointyCastleImpl({ + required Uint8List key, + required Uint8List iv, + required bool encrypt, + }) : _cipher = _createAESCipher(aesKey: key, iv: iv, encrypt: encrypt); + + static PaddedBlockCipherImpl _createAESCipher({ + required Uint8List aesKey, + required Uint8List iv, + required bool encrypt, + }) { + final cbcCipher = CBCBlockCipher(AESEngine()); + return PaddedBlockCipherImpl(PKCS7Padding(), cbcCipher) + ..init( + encrypt, + PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(aesKey), iv), + null, + ), + ); + } + + final PaddedBlockCipherImpl _cipher; + Uint8List? _carry; + List? _preBytes; + + @override + void update(Uint8List bytes, OnCipherCallback onCipher) { + final toProcess = _preBytes; + _preBytes = bytes; + if (toProcess == null) { + return; + } + final Uint8List data; + if (_carry == null) { + data = Uint8List.fromList(toProcess); + } else { + data = Uint8List.fromList(_carry! + toProcess); + _carry = null; + } + final length = data.length - (data.length % 1024); + if (length < data.length) { + _carry = data.sublist(length); + } else { + _carry = null; + } + final encryptedData = Uint8List(length); + var offset = 0; + while (offset < length) { + offset += _cipher.processBlock(data, offset, encryptedData, offset); + } + onCipher(encryptedData); + } + + @override + void finish(OnCipherCallback onCipher) { + final Uint8List lastBlock; + if (_carry == null) { + lastBlock = Uint8List.fromList(_preBytes ?? []); + } else { + lastBlock = Uint8List.fromList(_carry! + _preBytes!); + } + final encryptedData = _cipher.process(lastBlock); + onCipher(encryptedData); + } +} diff --git a/lib/utils/crypto/hmac.dart b/lib/utils/crypto/hmac.dart new file mode 100644 index 0000000000..51bc0afccd --- /dev/null +++ b/lib/utils/crypto/hmac.dart @@ -0,0 +1,74 @@ +import 'dart:typed_data'; + +import 'package:common_crypto/common_crypto.dart'; +import 'package:pointycastle/digests/sha256.dart'; +import 'package:pointycastle/macs/hmac.dart'; +import 'package:pointycastle/pointycastle.dart'; + +import '../platform.dart'; + +Uint8List calculateHMac(Uint8List key, Uint8List data) { + final calculator = HMacCalculator(key)..addBytes(data); + return calculator.result; +} + +abstract class HMacCalculator { + factory HMacCalculator(Uint8List key) { + if (kPlatformIsDarwin) { + return _HMacCalculatorCommonCrypto(key); + } + return _HMacCalculatorPointyCastleImpl(key); + } + + void addBytes(Uint8List data); + + Uint8List get result; +} + +class _HMacCalculatorPointyCastleImpl implements HMacCalculator { + _HMacCalculatorPointyCastleImpl(this._hMacKey) + : _hmac = HMac(SHA256Digest(), 64) { + _hmac.init(KeyParameter(_hMacKey)); + } + + final Uint8List _hMacKey; + final HMac _hmac; + + @override + void addBytes(Uint8List data) { + _hmac.update(data, 0, data.length); + } + + @override + Uint8List get result { + final bytes = Uint8List(_hmac.macSize); + final len = _hmac.doFinal(bytes, 0); + return bytes.sublist(0, len); + } +} + +class _HMacCalculatorCommonCrypto implements HMacCalculator { + _HMacCalculatorCommonCrypto(Uint8List key) : _hmac = HMacSha256(key); + + final HMacSha256 _hmac; + var _isDisposed = false; + + @override + void addBytes(Uint8List data) { + if (_isDisposed) { + throw StateError('HMacCalculator is disposed'); + } + _hmac.update(data); + } + + @override + Uint8List get result { + if (_isDisposed) { + throw StateError('HMacCalculator is disposed'); + } + final result = _hmac.finalize(); + _hmac.dispose(); + _isDisposed = true; + return result; + } +} diff --git a/lib/utils/device_transfer/cipher.dart b/lib/utils/device_transfer/cipher.dart index cb0469a2d1..b5ada77298 100644 --- a/lib/utils/device_transfer/cipher.dart +++ b/lib/utils/device_transfer/cipher.dart @@ -3,9 +3,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:libsignal_protocol_dart/src/kdf/hkdfv3.dart'; -import 'package:pointycastle/digests/sha256.dart'; -import 'package:pointycastle/macs/hmac.dart'; -import 'package:pointycastle/pointycastle.dart'; import '../crypto_util.dart'; @@ -28,31 +25,7 @@ TransferSecretKey generateTransferKey() { return TransferSecretKey(bytes); } -Uint8List calculateHMac(Uint8List key, Uint8List data) { - final calculator = HMacCalculator(key)..addBytes(data); - return calculator.result; -} - Uint8List generateTransferIv() { const _kIVBytesCount = 16; return Uint8List.fromList(generateRandomKey(_kIVBytesCount)); } - -class HMacCalculator { - HMacCalculator(this._hMacKey) : _hmac = HMac.withDigest(SHA256Digest()) { - _hmac.init(KeyParameter(_hMacKey)); - } - - final Uint8List _hMacKey; - final HMac _hmac; - - void addBytes(Uint8List data) { - _hmac.update(data, 0, data.length); - } - - Uint8List get result { - final bytes = Uint8List(_hmac.macSize); - final len = _hmac.doFinal(bytes, 0); - return bytes.sublist(0, len); - } -} diff --git a/lib/utils/device_transfer/transfer_protocol.dart b/lib/utils/device_transfer/transfer_protocol.dart index e6dab3efaa..2b7a9e2588 100644 --- a/lib/utils/device_transfer/transfer_protocol.dart +++ b/lib/utils/device_transfer/transfer_protocol.dart @@ -5,13 +5,10 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; -import 'package:pointycastle/block/aes.dart'; -import 'package:pointycastle/block/modes/cbc.dart'; -import 'package:pointycastle/padded_block_cipher/padded_block_cipher_impl.dart'; -import 'package:pointycastle/paddings/pkcs7.dart'; -import 'package:pointycastle/pointycastle.dart'; import 'package:uuid/uuid.dart'; +import '../crypto/aes.dart'; +import '../crypto/hmac.dart'; import '../logger.dart'; import 'cipher.dart'; import 'json_transfer_data.dart'; @@ -44,23 +41,6 @@ sealed class TransferPacket { const _kIVBytesCount = 16; -@visibleForTesting -PaddedBlockCipherImpl createAESCipher({ - required Uint8List aesKey, - required Uint8List iv, - required bool encrypt, -}) { - final cbcCipher = CBCBlockCipher(AESEngine()); - return PaddedBlockCipherImpl(PKCS7Padding(), cbcCipher) - ..init( - encrypt, - PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(aesKey), iv), - null, - ), - ); -} - Future _writeDataToSink({ required TransferSocket sink, required TransferSecretKey key, @@ -69,9 +49,7 @@ Future _writeDataToSink({ }) async { final iv = generateTransferIv(); - final aesCipher = createAESCipher(aesKey: key.aesKey, iv: iv, encrypt: true); - - final encryptedData = aesCipher.process(data); + final encryptedData = AesCipher.encrypt(key: key.aesKey, iv: iv, data: data); final bodyLength = iv.length + encryptedData.length; @@ -171,8 +149,7 @@ class TransferAttachmentPacket extends TransferPacket { final hMacCalculator = HMacCalculator(key.hMacKey); final iv = generateTransferIv(); - final aesCipher = - createAESCipher(aesKey: key.aesKey, iv: iv, encrypt: true); + final aesCipher = AesCipher(key: key.aesKey, iv: iv, encrypt: true); final header = ByteData(5) ..setInt8(0, kTypeFile) @@ -191,51 +168,21 @@ class TransferAttachmentPacket extends TransferPacket { final fileStream = file.openRead(); - Uint8List? carry; - List? preBytes; await for (final bytes in fileStream) { - final toProcess = preBytes; - preBytes = bytes; - if (toProcess == null) { - continue; - } - final Uint8List data; - if (carry == null) { - data = Uint8List.fromList(toProcess); - } else { - data = Uint8List.fromList(carry + toProcess); - carry = null; - } - - final length = data.length - (data.length % 1024); - if (length < data.length) { - carry = data.sublist(length); - } else { - carry = null; - } - - final encryptedData = Uint8List(length); - var offset = 0; - while (offset < length) { - offset += aesCipher.processBlock(data, offset, encryptedData, offset); - } - hMacCalculator.addBytes(encryptedData); - sink.add(encryptedData); - actualEncryptedLength += encryptedData.length; + aesCipher.update(Uint8List.fromList(bytes), (data) { + hMacCalculator.addBytes(data); + sink.add(data); + actualEncryptedLength += data.length; + }); await sink.flush(); } // handle last block - final Uint8List lastBlock; - if (carry == null) { - lastBlock = Uint8List.fromList(preBytes ?? []); - } else { - lastBlock = Uint8List.fromList(carry + preBytes!); - } - final encryptedData = aesCipher.process(lastBlock); - hMacCalculator.addBytes(encryptedData); - sink.add(encryptedData); - actualEncryptedLength += encryptedData.length; + aesCipher.finish((data) { + hMacCalculator.addBytes(data); + sink.add(data); + actualEncryptedLength += data.length; + }); await sink.flush(); sink.add(hMacCalculator.result); @@ -305,9 +252,10 @@ class _TransferJsonPacketBuilder extends _TransferPacketBuilder { assert(_writeBodyLength == expectedBodyLength); final data = Uint8List.fromList(_body); final iv = Uint8List.sublistView(data, 0, _kIVBytesCount); - final aesCipher = createAESCipher(aesKey: aesKey, iv: iv, encrypt: false); - final jsonData = aesCipher.process( - Uint8List.sublistView(data, _kIVBytesCount), + final jsonData = AesCipher.decrypt( + key: aesKey, + iv: iv, + data: Uint8List.sublistView(data, _kIVBytesCount), ); try { return creator(jsonData); @@ -326,10 +274,7 @@ class _TransferAttachmentPacketBuilder extends _TransferPacketBuilder { File? _file; String? _messageId; - late BlockCipher? _aesCipher; - - Uint8List? _carry; - Uint8List? _preProcessData; + late AesCipher? _aesCipher; @override bool doWriteBody(Uint8List bytes) { @@ -348,7 +293,7 @@ class _TransferAttachmentPacketBuilder extends _TransferPacketBuilder { _kUUIDBytesCount, _kUUIDBytesCount + _kIVBytesCount, ); - _aesCipher = createAESCipher(aesKey: aesKey, iv: iv, encrypt: false); + _aesCipher = AesCipher(key: aesKey, iv: iv, encrypt: false); final tempFileName = const Uuid().v4(); final file = File(p.join(folder, tempFileName)); @@ -373,48 +318,16 @@ class _TransferAttachmentPacketBuilder extends _TransferPacketBuilder { } void _processData(Uint8List encryptedData) { - final toProcessData = _preProcessData; - _preProcessData = encryptedData; - if (toProcessData == null) { - return; - } - - final Uint8List data; - if (_carry != null) { - data = Uint8List.fromList(_carry! + toProcessData); - _carry = null; - } else { - data = toProcessData; - } - // take the block size of the cipher into account - final length = data.length - (data.length % 1024); - if (length < data.length) { - _carry = data.sublist(length); - } else { - _carry = null; - } - if (length <= 0) { - return; - } - final bytes = Uint8List(length); - var offset = 0; - while (offset < length) { - offset += _aesCipher!.processBlock(data, offset, bytes, offset); - } - _file!.writeAsBytesSync(bytes, mode: FileMode.append, flush: true); + _aesCipher!.update(encryptedData, (data) { + _file!.writeAsBytesSync(data, mode: FileMode.append, flush: true); + }); } @override TransferAttachmentPacket build() { - final Uint8List lastBlockData; - if (_carry != null) { - lastBlockData = Uint8List.fromList(_carry! + _preProcessData!); - _carry = null; - } else { - lastBlockData = _preProcessData!; - } - final bytes = _aesCipher!.process(lastBlockData); - _file!.writeAsBytesSync(bytes, mode: FileMode.append, flush: true); + _aesCipher!.finish((data) { + _file!.writeAsBytesSync(data, mode: FileMode.append, flush: true); + }); assert(_writeBodyLength == expectedBodyLength, 'writeBodyLength != expectedBodyLength'); diff --git a/pubspec.lock b/pubspec.lock index c1361779e8..a5dcac6292 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,10 +238,19 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" + common_crypto: + dependency: "direct main" + description: + path: "packages/common_crypto" + ref: "1ac0384083ca7d072f9d1246a0dd221e458d3169" + resolved-ref: "1ac0384083ca7d072f9d1246a0dd221e458d3169" + url: "https://github.com/MixinNetwork/flutter-plugins.git" + source: git + version: "0.0.1" console: dependency: transitive description: @@ -899,10 +908,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" intl_phone_number_input: dependency: "direct main" description: @@ -1043,18 +1052,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -1546,10 +1555,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sprintf: dependency: transitive description: @@ -1658,10 +1667,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa19e93e4b..4d831066f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,11 @@ dependencies: url: https://github.com/MixinNetwork/flutter-plugins.git ref: 904786942d698d103ef1f27b5e9331c6a6ec8a37 path: packages/bring_window_to_front + common_crypto: + git: + url: https://github.com/MixinNetwork/flutter-plugins.git + ref: 1ac0384083ca7d072f9d1246a0dd221e458d3169 + path: packages/common_crypto dbus: ^0.7.8 decimal: ^2.3.2 desktop_drop: ^0.4.1 diff --git a/test/utils/transfer_cipher_test.dart b/test/utils/transfer_cipher_test.dart index d6351195cc..91e328e96c 100644 --- a/test/utils/transfer_cipher_test.dart +++ b/test/utils/transfer_cipher_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:flutter_app/utils/device_transfer/cipher.dart'; +import 'package:flutter_app/utils/crypto/hmac.dart'; import 'package:flutter_test/flutter_test.dart'; void main() {