diff --git a/packages/at_chops/CHANGELOG.md b/packages/at_chops/CHANGELOG.md index 049f27bd..42b31488 100644 --- a/packages/at_chops/CHANGELOG.md +++ b/packages/at_chops/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.0 +- feat: Faster AES encryption/decryption using better_crypto ## 2.2.0 - feat: Implement "argon2id" hashing algorithm to generate hash from a given passphrase. - feat: Add generics to "AtEncryptionAlgorithm" and "AtHashingAlgorithm" to support multiple data types in their diff --git a/packages/at_chops/example/at_chops_example.dart b/packages/at_chops/example/at_chops_example.dart index f540670b..cba53060 100644 --- a/packages/at_chops/example/at_chops_example.dart +++ b/packages/at_chops/example/at_chops_example.dart @@ -41,11 +41,11 @@ void main(List args) async { final data = 'Hello World'; //1.1 encrypt the data using [atEncryptionKeyPair.publicKey] final encryptionResult = - atChops.encryptString(data, EncryptionKeyType.rsa2048); + await atChops.encryptString(data, EncryptionKeyType.rsa2048); //1.2 decrypt the data using [atEncryptionKeyPair.privateKey] - final decryptionResult = - atChops.decryptString(encryptionResult.result, EncryptionKeyType.rsa2048); + final decryptionResult = await atChops.decryptString( + encryptionResult.result, EncryptionKeyType.rsa2048); assert(data == decryptionResult.result, true); // 2 - Signing and data verification using asymmetric key pair diff --git a/packages/at_chops/lib/src/algorithm/aes_ctr_factory.dart b/packages/at_chops/lib/src/algorithm/aes_ctr_factory.dart new file mode 100644 index 00000000..712d145d --- /dev/null +++ b/packages/at_chops/lib/src/algorithm/aes_ctr_factory.dart @@ -0,0 +1,44 @@ +import 'package:at_chops/at_chops.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:better_cryptography/better_cryptography.dart'; + +/// A factory class to create AES-CTR encryption algorithms based on the key length. +/// +/// The `AesCtrFactory` class provides a static method to create an instance of +/// the `AesCtr` encryption algorithm. The key length of the provided `AESKey` +/// determines the specific variant of AES-CTR to be instantiated. +class AesCtrFactory { + /// Creates an `AesCtr` encryption algorithm based on the key length of the given [aesKey]. + /// + /// The `aesKey` must have a length of 16, 24, or 32 bytes to correspond to AES-128, AES-192, + /// or AES-256 respectively. A `MacAlgorithm.empty` is used for each variant. + /// + /// Throws an [AtEncryptionException] if the provided key length is invalid. + /// + /// Example usage: + /// ```dart + /// AESKey aesKey =AESKey.generate(32);//pass the length in bytes + /// AesCtr encryptionAlgo = AesCtrFactory.createEncryptionAlgo(aesKey); + /// ``` + /// + /// - [aesKey]: An instance of `AESKey` containing the encryption key. + /// - Returns: An instance of `AesCtr` configured for the appropriate key length. + /// + /// Supported key lengths: + /// - 16 bytes for AES-128 + /// - 24 bytes for AES-192 + /// - 32 bytes for AES-256 + static AesCtr createEncryptionAlgo(AESKey aesKey) { + switch (aesKey.getLength()) { + case 16: + return AesCtr.with128bits(macAlgorithm: MacAlgorithm.empty); + case 24: + return AesCtr.with192bits(macAlgorithm: MacAlgorithm.empty); + case 32: + return AesCtr.with256bits(macAlgorithm: MacAlgorithm.empty); + default: + throw AtEncryptionException( + 'Invalid AES key length. Valid lengths are 16/24/32 bytes'); + } + } +} diff --git a/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart b/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart index 06c42f8f..8524e189 100644 --- a/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart +++ b/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart @@ -1,32 +1,53 @@ +import 'dart:async'; import 'dart:typed_data'; - +import 'dart:convert'; +import 'package:at_chops/src/algorithm/aes_ctr_factory.dart'; +import 'package:at_chops/src/algorithm/padding/padding.dart'; import 'package:at_chops/at_chops.dart'; import 'package:at_chops/src/algorithm/at_algorithm.dart'; +import 'package:at_chops/src/algorithm/padding/padding_params.dart'; +import 'package:at_chops/src/algorithm/padding/pkcs7padding.dart'; import 'package:at_commons/at_commons.dart'; import 'package:encrypt/encrypt.dart'; +import 'package:better_cryptography/better_cryptography.dart'; /// A class that provides AES encryption and decryption for Uint8List, /// implementing the [SymmetricEncryptionAlgorithm] interface. class AESEncryptionAlgo implements SymmetricEncryptionAlgorithm { final AESKey _aesKey; - - AESEncryptionAlgo(this._aesKey); + PaddingAlgorithm? paddingAlgo; + AESEncryptionAlgo(this._aesKey) { + paddingAlgo ??= PKCS7Padding(PaddingParams()..blockSize = 16); + } @override - Uint8List encrypt(Uint8List plainData, {InitialisationVector? iv}) { - var aesEncrypter = Encrypter(AES(Key.fromBase64(_aesKey.key))); - final encrypted = - aesEncrypter.encryptBytes(plainData, iv: _getIVFromBytes(iv?.ivBytes)); - return encrypted.bytes; + FutureOr encrypt(Uint8List plainData, + {InitialisationVector? iv}) async { + final encryptionAlgo = AesCtrFactory.createEncryptionAlgo(_aesKey); + var paddedData = paddingAlgo!.addPadding(plainData); + final secretKey = await encryptionAlgo + .newSecretKeyFromBytes(base64Decode(_aesKey.toString())); + var secretBox = await encryptionAlgo.encrypt(paddedData, + nonce: _getIVFromBytes(iv?.ivBytes)!.bytes, secretKey: secretKey); + return Uint8List.fromList(secretBox.cipherText); } @override - Uint8List decrypt(Uint8List encryptedData, {InitialisationVector? iv}) { - var aesKey = AES(Key.fromBase64(_aesKey.toString())); - var decrypter = Encrypter(aesKey); - return Uint8List.fromList(decrypter.decryptBytes(Encrypted(encryptedData), - iv: _getIVFromBytes(iv?.ivBytes))); + FutureOr decrypt(Uint8List encryptedData, + {InitialisationVector? iv}) async { + final encryptionAlgo = AesCtrFactory.createEncryptionAlgo(_aesKey); + var secretBox = SecretBox( + encryptedData, + nonce: _getIVFromBytes(iv?.ivBytes)!.bytes, + mac: Mac.empty, + ); + final secretKey = await encryptionAlgo + .newSecretKeyFromBytes(base64Decode(_aesKey.toString())); + var decryptedBytesWithPadding = + await encryptionAlgo.decrypt(secretBox, secretKey: secretKey); + var decryptedBytes = paddingAlgo!.removePadding(decryptedBytesWithPadding); + return Uint8List.fromList(decryptedBytes); } IV? _getIVFromBytes(Uint8List? ivBytes) { diff --git a/packages/at_chops/lib/src/algorithm/at_algorithm.dart b/packages/at_chops/lib/src/algorithm/at_algorithm.dart index f41a6d71..d3e7bde8 100644 --- a/packages/at_chops/lib/src/algorithm/at_algorithm.dart +++ b/packages/at_chops/lib/src/algorithm/at_algorithm.dart @@ -11,20 +11,20 @@ import 'package:at_chops/src/model/hash_params.dart'; /// Interface for encrypting and decrypting data. Check [DefaultEncryptionAlgo] for sample implementation. abstract class AtEncryptionAlgorithm { /// Encrypts the passed bytes. Bytes are passed as [Uint8List]. Encode String data type to [Uint8List] using [utf8.encode]. - V encrypt(T plainData); + FutureOr encrypt(T plainData); /// Decrypts the passed encrypted bytes. - V decrypt(T encryptedData); + FutureOr decrypt(T encryptedData); } /// Interface for symmetric encryption algorithms. Check [AESEncryptionAlgo] for sample implementation. abstract class SymmetricEncryptionAlgorithm extends AtEncryptionAlgorithm { @override - V encrypt(T plainData, {InitialisationVector iv}); + FutureOr encrypt(T plainData, {InitialisationVector iv}); @override - V decrypt(T encryptedData, {InitialisationVector iv}); + FutureOr decrypt(T encryptedData, {InitialisationVector iv}); } /// Interface for asymmetric encryption algorithms. Check [DefaultEncryptionAlgo] for sample implementation. diff --git a/packages/at_chops/lib/src/algorithm/padding/padding.dart b/packages/at_chops/lib/src/algorithm/padding/padding.dart new file mode 100644 index 00000000..124b6d25 --- /dev/null +++ b/packages/at_chops/lib/src/algorithm/padding/padding.dart @@ -0,0 +1,38 @@ +/// An abstract class that defines a padding algorithm for AES encryption and decryption. +/// +/// The `PaddingAlgorithm` class provides methods to add padding bytes during encryption +/// and to remove padding bytes during decryption. Padding ensures that the input data +/// size is compatible with the block size required by AES encryption algorithms. +abstract class PaddingAlgorithm { + /// Adds padding bytes to the given [data] to make its length a multiple of the AES block size. + /// + /// This method appends padding bytes to the input data so that its length becomes + /// a multiple of the AES block size (16 bytes). The exact padding scheme is + /// implementation-dependent. + /// + /// - [data]: A list of bytes representing the input data to be padded. + /// - Returns: A new list of bytes containing the padded data. + /// + /// Example usage: + /// ```dart + /// PaddingAlgorithm paddingAlgorithm = PKCS7Padding(); + /// List paddedData = paddingAlgorithm.addPadding([0x01, 0x02, 0x03]); + /// ``` + List addPadding(List data); + + /// Removes padding bytes from the given [data], restoring it to its original unpadded form. + /// + /// This method removes any padding bytes that were added during encryption to return + /// the data to its original state. The exact removal logic depends on the padding + /// scheme used during encryption. + /// + /// - [data]: A list of bytes representing the padded input data. + /// - Returns: A new list of bytes containing the original unpadded data. + /// + /// Example usage: + /// ```dart + /// PaddingAlgorithm paddingAlgorithm = PKCS7Padding(); + /// List unpaddedData = paddingAlgorithm.removePadding([0x01, 0x02, 0x03, 0x05, 0x05, 0x05]); + /// ``` + List removePadding(List data); +} diff --git a/packages/at_chops/lib/src/algorithm/padding/padding_params.dart b/packages/at_chops/lib/src/algorithm/padding/padding_params.dart new file mode 100644 index 00000000..ca7a4617 --- /dev/null +++ b/packages/at_chops/lib/src/algorithm/padding/padding_params.dart @@ -0,0 +1,13 @@ +/// A class that defines parameters for padding algorithms used in AES encryption. +/// +/// The `PaddingParams` class provides configurable parameters required for +/// padding algorithms, such as the block size. These parameters are used to +/// ensure that data conforms to the block size required by AES encryption. +class PaddingParams { + /// The block size (in bytes) used for padding. + /// + /// The default value is `16`, which corresponds to the block size of AES encryption. + /// This value determines the size to which input data will be padded to ensure + /// compatibility with the encryption algorithm. + int blockSize = 16; +} diff --git a/packages/at_chops/lib/src/algorithm/padding/pkcs7padding.dart b/packages/at_chops/lib/src/algorithm/padding/pkcs7padding.dart new file mode 100644 index 00000000..b2f9172c --- /dev/null +++ b/packages/at_chops/lib/src/algorithm/padding/pkcs7padding.dart @@ -0,0 +1,49 @@ +import 'package:at_chops/src/algorithm/padding/padding.dart'; +import 'package:at_chops/src/algorithm/padding/padding_params.dart'; +import 'package:at_commons/at_commons.dart'; + +class PKCS7Padding implements PaddingAlgorithm { + final PaddingParams _paddingParams; + PKCS7Padding(this._paddingParams); + @override + List addPadding(List data) { + if (_paddingParams.blockSize <= 0 || _paddingParams.blockSize > 255) { + throw AtEncryptionException('Block size must be between 1 and 255.'); + } + + // Calculate the number of padding bytes needed + int padding = + _paddingParams.blockSize - (data.length % _paddingParams.blockSize); + + // Add padding bytes to the data + List paddedData = List.from(data); + paddedData.addAll(List.filled(padding, padding)); + + return paddedData; + } + + @override + List removePadding(List data) { + if (data.isEmpty) { + throw AtDecryptionException('Encrypted data cannot be empty'); + } + + // Get the value of the last byte (padding length) + int paddingLength = data.last; + + // Validate padding length + if (paddingLength <= 0 || paddingLength > data.length) { + throw AtDecryptionException('Invalid padding length'); + } + + // Check if all padding bytes are valid + for (int i = data.length - paddingLength; i < data.length; i++) { + if (data[i] != paddingLength) { + throw AtDecryptionException('Invalid PKCS7 padding'); + } + } + + // Return the data without padding + return data.sublist(0, data.length - paddingLength); + } +} diff --git a/packages/at_chops/lib/src/at_chops_base.dart b/packages/at_chops/lib/src/at_chops_base.dart index 02bb2f1a..19430ed4 100644 --- a/packages/at_chops/lib/src/at_chops_base.dart +++ b/packages/at_chops/lib/src/at_chops_base.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:at_chops/at_chops.dart'; @@ -37,7 +38,7 @@ abstract class AtChops { /// If [encryptionKeyType] is [EncryptionKeyType.rsa2048] then [encryptionAlgorithm] will be set to [RsaEncryptionAlgo] /// [keyName] specifies which key pair to use if user has multiple key pairs configured. /// If [keyName] is not passed default encryption/decryption keypair from .atKeys file will be used. - AtEncryptionResult encryptBytes( + FutureOr encryptBytes( Uint8List data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, @@ -47,7 +48,7 @@ abstract class AtChops { /// If [encryptionKeyType] is [EncryptionKeyType.rsa2048] then [encryptionAlgorithm] will be set to [RsaEncryptionAlgo] /// [keyName] specifies which key pair to use if user has multiple key pairs configured. /// If [keyName] is not passed default encryption/decryption keypair from .atKeys file will be used. - AtEncryptionResult encryptString( + FutureOr encryptString( String data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, @@ -57,7 +58,7 @@ abstract class AtChops { /// If [encryptionKeyType] is [EncryptionKeyType.rsa2048] then [encryptionAlgorithm] will be set to [RsaEncryptionAlgo] /// [keyName] specifies which key pair to use if user has multiple key pairs configured. /// If [keyName] is not passed default encryption/decryption keypair from .atKeys file will be used. - AtEncryptionResult decryptBytes( + FutureOr decryptBytes( Uint8List data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, @@ -67,7 +68,7 @@ abstract class AtChops { /// If [encryptionKeyType] is [EncryptionKeyType.rsa2048] then [encryptionAlgorithm] will be set to [RsaEncryptionAlgo] /// [keyName] specifies which key pair to use if user has multiple key pairs configured. /// If [keyName] is not passed default encryption/decryption keypair from .atKeys file will be used. - AtEncryptionResult decryptString( + FutureOr decryptString( String data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, diff --git a/packages/at_chops/lib/src/at_chops_impl.dart b/packages/at_chops/lib/src/at_chops_impl.dart index 55b18450..808c46f8 100644 --- a/packages/at_chops/lib/src/at_chops_impl.dart +++ b/packages/at_chops/lib/src/at_chops_impl.dart @@ -1,5 +1,6 @@ // ignore_for_file: unnecessary_cast +import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; @@ -33,11 +34,11 @@ class AtChopsImpl extends AtChops { final AtSignLogger _logger = AtSignLogger('AtChopsImpl'); @override - AtEncryptionResult decryptBytes( + FutureOr decryptBytes( Uint8List data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, - InitialisationVector? iv}) { + InitialisationVector? iv}) async { try { encryptionAlgorithm ??= _getEncryptionAlgorithm(encryptionKeyType, keyName)!; @@ -52,7 +53,8 @@ class AtChopsImpl extends AtChops { ..atEncryptionMetaData = atEncryptionMetaData ..atEncryptionResultType = AtEncryptionResultType.bytes; if (encryptionAlgorithm is SymmetricEncryptionAlgorithm) { - atEncryptionResult.result = encryptionAlgorithm.decrypt(data, iv: iv!); + atEncryptionResult.result = + await encryptionAlgorithm.decrypt(data, iv: iv!); atEncryptionMetaData.iv = iv; } else { atEncryptionResult.result = encryptionAlgorithm.decrypt(data); @@ -70,13 +72,13 @@ class AtChopsImpl extends AtChops { /// Decode the encrypted string to base64. /// Decode the encrypted byte to utf8 to support emoji chars. @override - AtEncryptionResult decryptString( + FutureOr decryptString( String data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, - InitialisationVector? iv}) { + InitialisationVector? iv}) async { try { - final decryptionResult = decryptBytes( + final decryptionResult = await decryptBytes( base64Decode(data), encryptionKeyType, encryptionAlgorithm: encryptionAlgorithm, keyName: keyName, iv: iv); final atEncryptionResult = AtEncryptionResult() @@ -90,11 +92,11 @@ class AtChopsImpl extends AtChops { } @override - AtEncryptionResult encryptBytes( + FutureOr encryptBytes( Uint8List data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, - InitialisationVector? iv}) { + InitialisationVector? iv}) async { try { encryptionAlgorithm ??= _getEncryptionAlgorithm(encryptionKeyType, keyName)!; @@ -105,7 +107,8 @@ class AtChopsImpl extends AtChops { ..atEncryptionMetaData = atEncryptionMetaData ..atEncryptionResultType = AtEncryptionResultType.bytes; if (encryptionAlgorithm is SymmetricEncryptionAlgorithm) { - atEncryptionResult.result = encryptionAlgorithm.encrypt(data, iv: iv!); + atEncryptionResult.result = + await encryptionAlgorithm.encrypt(data, iv: iv!); atEncryptionMetaData.iv = iv; } else { atEncryptionResult.result = encryptionAlgorithm.encrypt(data); @@ -123,14 +126,14 @@ class AtChopsImpl extends AtChops { /// Encode the input string to utf8 to support emoji chars. /// Encode the encrypted bytes to base64. @override - AtEncryptionResult encryptString( + FutureOr encryptString( String data, EncryptionKeyType encryptionKeyType, {AtEncryptionAlgorithm? encryptionAlgorithm, String? keyName, - InitialisationVector? iv}) { + InitialisationVector? iv}) async { try { final utfEncodedData = utf8.encode(data); - final encryptionResult = encryptBytes( + final encryptionResult = await encryptBytes( Uint8List.fromList(utfEncodedData), encryptionKeyType, keyName: keyName, encryptionAlgorithm: encryptionAlgorithm, iv: iv); final atEncryptionResult = AtEncryptionResult() diff --git a/packages/at_chops/lib/src/key/impl/aes_key.dart b/packages/at_chops/lib/src/key/impl/aes_key.dart index 1d066a38..cc6c2ec0 100644 --- a/packages/at_chops/lib/src/key/impl/aes_key.dart +++ b/packages/at_chops/lib/src/key/impl/aes_key.dart @@ -1,5 +1,6 @@ import 'package:at_chops/src/key/at_key_pair.dart'; import 'package:encrypt/encrypt.dart'; +import 'dart:convert'; /// Represents an AES key for symmetric encryption. class AESKey extends SymmetricKey { @@ -15,6 +16,14 @@ class AESKey extends SymmetricKey { return AESKey(aesKey.key.base64); } + /// Returns the key length in bytes. + /// e.g for 128 bit key length will be 16 + /// for 192 bit key length will be 24 + /// for 256 bit key length will be 32 + int getLength() { + return base64.decode(_aesKey).length; + } + @override String toString() { return _aesKey; diff --git a/packages/at_chops/pubspec.yaml b/packages/at_chops/pubspec.yaml index 2999b53a..9a9ec254 100644 --- a/packages/at_chops/pubspec.yaml +++ b/packages/at_chops/pubspec.yaml @@ -1,6 +1,6 @@ name: at_chops description: Package for at_protocol cryptographic and hashing operations -version: 2.2.0 +version: 3.0.0 repository: https://github.com/atsign-foundation/at_libraries environment: @@ -14,10 +14,11 @@ dependencies: ecdsa: ^0.1.0 dart_periphery: ^0.9.5 elliptic: ^0.3.10 - pointycastle: ^3.7.4 + pointycastle: ^3.9.1 at_commons: ^5.0.2 at_utils: ^3.0.19 cryptography: ^2.7.0 + better_cryptography: ^1.0.0+1 dev_dependencies: lints: ^5.0.0 diff --git a/packages/at_chops/test/aes_encrption_old_impl.dart b/packages/at_chops/test/aes_encrption_old_impl.dart new file mode 100644 index 00000000..6ad7b0ef --- /dev/null +++ b/packages/at_chops/test/aes_encrption_old_impl.dart @@ -0,0 +1,103 @@ +import 'dart:typed_data'; + +import 'package:at_chops/at_chops.dart'; +import 'package:at_chops/src/algorithm/at_algorithm.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:encrypt/encrypt.dart'; + +/// Old AES implementation. Used for testing backward compatibility. +class AESEncryptionAlgoV1 + implements SymmetricEncryptionAlgorithm { + final AESKey _aesKey; + + AESEncryptionAlgoV1(this._aesKey); + + @override + Uint8List encrypt(Uint8List plainData, {InitialisationVector? iv}) { + var aesEncrypter = Encrypter(AES(Key.fromBase64(_aesKey.key))); + final encrypted = + aesEncrypter.encryptBytes(plainData, iv: _getIVFromBytes(iv?.ivBytes)); + return encrypted.bytes; + } + + @override + Uint8List decrypt(Uint8List encryptedData, {InitialisationVector? iv}) { + var aesKey = AES(Key.fromBase64(_aesKey.toString())); + var decrypter = Encrypter(aesKey); + return Uint8List.fromList(decrypter.decryptBytes(Encrypted(encryptedData), + iv: _getIVFromBytes(iv?.ivBytes))); + } + + IV? _getIVFromBytes(Uint8List? ivBytes) { + if (ivBytes != null) { + return IV(ivBytes); + } + // From the bad old days when we weren't setting IVs + return IV(Uint8List(16)); + } +} + +/// A class that provides AES encryption and decryption for strings, +/// implementing the [SymmetricEncryptionAlgorithm] interface. +/// +/// This class uses an [AESKey] to perform encryption and decryption of strings. +/// The key and an [InitialisationVector] (IV) are used for encryption, and the +/// same key must be used for decryption. +class StringAESEncryptor + implements SymmetricEncryptionAlgorithm { + /// The AES key used for encryption and decryption. + final AESKey _aesKey; + + /// Constructs an instance of [StringAESEncryptor] with the provided [_aesKey]. + /// + /// [_aesKey]: The key used for AES encryption and decryption, represented + /// in Base64 format. + StringAESEncryptor(this._aesKey); + + /// Decrypts the given [encryptedData] using the provided [iv] (Initialisation Vector). + /// + /// The [iv] used for encryption must be the same for decryption. If [iv] is + /// not provided, an [AtDecryptionException] will be thrown, as the IV is + /// mandatory for the AES decryption process. + /// + /// - [encryptedData]: The Base64-encoded string that represents the encrypted data. + /// - [iv]: The Initialisation Vector used during decryption. Must be the same + /// IV that was used to encrypt the data. + /// + /// Returns a [String] that represents the decrypted data. + /// + /// Throws an [AtDecryptionException] if the [iv] is missing. + + @override + String decrypt(String encryptedData, {InitialisationVector? iv}) { + // The IV used for encryption, the same IV must be used for decryption. + if (iv == null) { + throw AtDecryptionException( + 'Initialisation Vector (IV) is required for decryption'); + } + var aesEncrypter = Encrypter(AES(Key.fromBase64(_aesKey.key))); + return aesEncrypter.decrypt(Encrypted.fromBase64(encryptedData), + iv: IV(iv.ivBytes)); + } + + /// Encrypts the given [plainData] using AES encryption and an optional [iv]. + /// The resulting encrypted data will be Base64-encoded. + /// + /// - [plainData]: The string that needs to be encrypted. + /// - [iv]: The Initialisation Vector used for encryption. If not provided, + /// AtEncryptionException will be thrown. + /// + /// Returns a [String] that contains the encrypted data, encoded in Base64 format. + /// + /// Throws an [AtEncryptionException] if the [iv] is missing. + @override + String encrypt(String plainData, {InitialisationVector? iv}) { + if (iv == null) { + throw AtEncryptionException( + 'Initialisation Vector (IV) is required for encryption'); + } + var aesEncrypter = Encrypter(AES(Key.fromBase64(_aesKey.key))); + final encrypted = aesEncrypter.encrypt(plainData, iv: IV(iv.ivBytes)); + return encrypted.base64; + } +} diff --git a/packages/at_chops/test/aes_encryption_algo_test.dart b/packages/at_chops/test/aes_encryption_algo_test.dart new file mode 100644 index 00000000..b5138829 --- /dev/null +++ b/packages/at_chops/test/aes_encryption_algo_test.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'package:at_chops/at_chops.dart'; +import 'package:test/test.dart'; +import 'aes_encrption_old_impl.dart'; + +void main() { + group( + 'A group of tests to verify compatibility between old AES algo vs better crypto AES algo', + () { + test('Encrypt with old algo and decrypt with better crypto', () async { + var data = 'Hello World'; + var aesKey = AESKey.generate(32); + var iv = AtChopsUtil.generateRandomIV(16); + final encryptionAlgo = AESEncryptionAlgoV1(aesKey); + var encryptedBytes = encryptionAlgo.encrypt(utf8.encode(data), iv: iv); + var betterCryptoAESAlgo = AESEncryptionAlgo(aesKey); + var decryptedBytes = + await betterCryptoAESAlgo.decrypt(encryptedBytes, iv: iv); + expect(utf8.decode(decryptedBytes), data); + }); + test('Encrypt with better crypto AES algo and decrypt with old algo', + () async { + var data = 'Hello World12345'; + var aesKey = AESKey.generate(32); + var iv = AtChopsUtil.generateRandomIV(16); + final betterCryptoAESAlgo = AESEncryptionAlgo(aesKey); + var encryptedBytes = + await betterCryptoAESAlgo.encrypt(utf8.encode(data), iv: iv); + var oldAlgo = AESEncryptionAlgoV1(aesKey); + var decryptedBytes = oldAlgo.decrypt(encryptedBytes, iv: iv); + expect(utf8.decode(decryptedBytes), data); + }); + }); + group( + 'A group of tests to verify AES encryption decryption with different key lengths', + () { + test('Test encryption and decryption for 128 bit AES key', () async { + var data = 'Hello World🛠'; + var aesKey = AESKey.generate(16); + var iv = AtChopsUtil.generateRandomIV(16); + final betterCryptoAESAlgo = AESEncryptionAlgo(aesKey); + var encryptedBytes = + await betterCryptoAESAlgo.encrypt(utf8.encode(data), iv: iv); + var decryptedBytes = + await betterCryptoAESAlgo.decrypt(encryptedBytes, iv: iv); + expect(utf8.decode(decryptedBytes), data); + }); + test('Test encryption and decryption for 192 bit AES key', () async { + var data = 'Hello\nWorld🛠\n123asdasd!@&^'; + var aesKey = AESKey.generate(24); + var iv = AtChopsUtil.generateRandomIV(16); + final betterCryptoAESAlgo = AESEncryptionAlgo(aesKey); + var encryptedBytes = + await betterCryptoAESAlgo.encrypt(utf8.encode(data), iv: iv); + var decryptedBytes = + await betterCryptoAESAlgo.decrypt(encryptedBytes, iv: iv); + expect(utf8.decode(decryptedBytes), data); + }); + test('Test encryption and decryption for 256 bit AES key', () async { + var data = '🛠Hello\nWorld🛠\n123asdasd!@&^\'🛠'; + var aesKey = AESKey.generate(32); + var iv = AtChopsUtil.generateRandomIV(16); + final betterCryptoAESAlgo = AESEncryptionAlgo(aesKey); + var encryptedBytes = + await betterCryptoAESAlgo.encrypt(utf8.encode(data), iv: iv); + var decryptedBytes = + await betterCryptoAESAlgo.decrypt(encryptedBytes, iv: iv); + expect(utf8.decode(decryptedBytes), data); + }); + }); +} diff --git a/packages/at_chops/test/aes_key_test.dart b/packages/at_chops/test/aes_key_test.dart index b2cc3e1f..76e58d11 100644 --- a/packages/at_chops/test/aes_key_test.dart +++ b/packages/at_chops/test/aes_key_test.dart @@ -22,5 +22,32 @@ void main() { final aesKey_2 = AESKey.generate(32); expect(aesKey_1, isNot(aesKey_2)); }); + test('check random key generated length for 128 bit key', () { + final aesKey = AESKey.generate(16); + expect(aesKey.getLength(), 16); + }); + test('check random key generated length for 192 bit key', () { + final aesKey = AESKey.generate(24); + expect(aesKey.getLength(), 24); + }); + test('check random key generated length for 256 bit key', () { + final aesKey = AESKey.generate(32); + expect(aesKey.getLength(), 32); + }); + test('verify key length for 256 bit key constructed from string', () { + final aesKey_1 = AESKey.generate(32); + final aesKey = AESKey(aesKey_1.key); + expect(aesKey.getLength(), 32); + }); + test('verify key length for 192 bit key constructed from string', () { + final aesKey_1 = AESKey.generate(24); + final aesKey = AESKey(aesKey_1.key); + expect(aesKey.getLength(), 24); + }); + test('verify key length for 128 bit key constructed from string', () { + final aesKey_1 = AESKey.generate(16); + final aesKey = AESKey(aesKey_1.key); + expect(aesKey.getLength(), 16); + }); }); } diff --git a/packages/at_chops/test/at_chops_test.dart b/packages/at_chops/test/at_chops_test.dart index 24b07d8e..f799be1d 100644 --- a/packages/at_chops/test/at_chops_test.dart +++ b/packages/at_chops/test/at_chops_test.dart @@ -11,14 +11,14 @@ import 'package:test/test.dart'; void main() { AtSignLogger.root_level = 'finest'; group('A group of tests for encryption and decryption', () { - test('Test rsa encryption/decryption string', () { + test('Test rsa encryption/decryption string', () async { final atEncryptionKeyPair = AtChopsUtil.generateAtEncryptionKeyPair(); final atChopsKeys = AtChopsKeys.create(atEncryptionKeyPair, null); final atChops = AtChopsImpl(atChopsKeys); final data = 'Hello World'; final encryptionResult = - atChops.encryptString(data, EncryptionKeyType.rsa2048); + await atChops.encryptString(data, EncryptionKeyType.rsa2048); expect(encryptionResult.atEncryptionMetaData, isNotNull); expect(encryptionResult.result, isNotEmpty); expect(encryptionResult.atEncryptionMetaData.encryptionKeyType, @@ -26,7 +26,7 @@ void main() { expect(encryptionResult.atEncryptionMetaData.atEncryptionAlgorithm, 'RsaEncryptionAlgo'); - final decryptionResult = atChops.decryptString( + final decryptionResult = await atChops.decryptString( encryptionResult.result, EncryptionKeyType.rsa2048); expect(decryptionResult.atEncryptionMetaData, isNotNull); expect(decryptionResult.result, isNotEmpty); @@ -37,14 +37,15 @@ void main() { expect(decryptionResult.result, data); }); - test('Test symmetric encrypt/decrypt bytes with initialisation vector', () { + test('Test symmetric encrypt/decrypt bytes with initialisation vector', + () async { String data = 'Hello World'; final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; final atChops = AtChopsImpl(atChopsKeys); final iv = AtChopsUtil.generateRandomIV(16); - final encryptionResult = atChops.encryptBytes( + final encryptionResult = await atChops.encryptBytes( // ignore: unnecessary_cast utf8.encode(data) as Uint8List, EncryptionKeyType.aes256, @@ -57,7 +58,7 @@ void main() { 'AESEncryptionAlgo'); expect(encryptionResult.atEncryptionMetaData.iv, iv); - final decryptionResult = atChops.decryptBytes( + final decryptionResult = await atChops.decryptBytes( encryptionResult.result, EncryptionKeyType.aes256, iv: iv); expect(decryptionResult.result, isNotEmpty); @@ -69,14 +70,14 @@ void main() { expect(utf8.decode(decryptionResult.result), data); }); - test('Test symmetric encrypt/decrypt bytes with emoji char', () { + test('Test symmetric encrypt/decrypt bytes with emoji char', () async { String data = 'Hello World🛠'; final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; final atChops = AtChopsImpl(atChopsKeys); final iv = AtChopsUtil.generateRandomIV(16); - final encryptionResult = atChops.encryptBytes( + final encryptionResult = await atChops.encryptBytes( // ignore: unnecessary_cast utf8.encode(data) as Uint8List, EncryptionKeyType.aes256, @@ -89,7 +90,7 @@ void main() { 'AESEncryptionAlgo'); expect(encryptionResult.atEncryptionMetaData.iv, iv); - final decryptionResult = atChops.decryptBytes( + final decryptionResult = await atChops.decryptBytes( encryptionResult.result, EncryptionKeyType.aes256, iv: iv); expect(decryptionResult.result, isNotEmpty); @@ -101,14 +102,14 @@ void main() { expect(utf8.decode(decryptionResult.result), data); }); - test('Test symmetric encrypt/decrypt bytes with special chars', () { + test('Test symmetric encrypt/decrypt bytes with special chars', () async { String data = 'Hello World🛠'; final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; final atChops = AtChopsImpl(atChopsKeys); final iv = AtChopsUtil.generateRandomIV(16); - final encryptionResult = atChops.encryptBytes( + final encryptionResult = await atChops.encryptBytes( // ignore: unnecessary_cast utf8.encode(data) as Uint8List, EncryptionKeyType.aes256, @@ -121,7 +122,7 @@ void main() { 'AESEncryptionAlgo'); expect(encryptionResult.atEncryptionMetaData.iv, iv); - final decryptionResult = atChops.decryptBytes( + final decryptionResult = await atChops.decryptBytes( encryptionResult.result, EncryptionKeyType.aes256, iv: iv); expect(decryptionResult.result, isNotEmpty); @@ -134,7 +135,7 @@ void main() { }); test('Test symmetric encrypt/decrypt string with initialisation vector', - () { + () async { String data = 'Hello World'; final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; @@ -142,7 +143,7 @@ void main() { final iv = AtChopsUtil.generateRandomIV(16); final encryptionResult = - atChops.encryptString(data, EncryptionKeyType.aes256, iv: iv); + await atChops.encryptString(data, EncryptionKeyType.aes256, iv: iv); expect(encryptionResult.atEncryptionMetaData, isNotNull); expect(encryptionResult.result, isNotEmpty); expect(encryptionResult.atEncryptionMetaData.encryptionKeyType, @@ -151,7 +152,7 @@ void main() { 'AESEncryptionAlgo'); expect(encryptionResult.atEncryptionMetaData.iv, iv); - final decryptionResult = atChops.decryptString( + final decryptionResult = await atChops.decryptString( encryptionResult.result, EncryptionKeyType.aes256, iv: iv); expect(decryptionResult.result, isNotEmpty); @@ -163,7 +164,7 @@ void main() { expect(decryptionResult.result, data); }); - test('Test symmetric encrypt/decrypt string with special chars', () { + test('Test symmetric encrypt/decrypt string with special chars', () async { String data = 'Hello``*+%'; final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; @@ -171,7 +172,7 @@ void main() { final iv = AtChopsUtil.generateRandomIV(16); final encryptionResult = - atChops.encryptString(data, EncryptionKeyType.aes256, iv: iv); + await atChops.encryptString(data, EncryptionKeyType.aes256, iv: iv); expect(encryptionResult.atEncryptionMetaData, isNotNull); expect(encryptionResult.result, isNotEmpty); expect(encryptionResult.atEncryptionMetaData.encryptionKeyType, @@ -180,7 +181,7 @@ void main() { 'AESEncryptionAlgo'); expect(encryptionResult.atEncryptionMetaData.iv, iv); - final decryptionResult = atChops.decryptString( + final decryptionResult = await atChops.decryptString( encryptionResult.result, EncryptionKeyType.aes256, iv: iv); expect(decryptionResult.result, isNotEmpty); @@ -192,7 +193,7 @@ void main() { expect(decryptionResult.result, data); }); - test('Test symmetric encrypt/decrypt string with emoji', () { + test('Test symmetric encrypt/decrypt string with emoji', () async { String data = 'Hello World🛠'; final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; @@ -200,7 +201,7 @@ void main() { final iv = AtChopsUtil.generateRandomIV(16); final encryptionResult = - atChops.encryptString(data, EncryptionKeyType.aes256, iv: iv); + await atChops.encryptString(data, EncryptionKeyType.aes256, iv: iv); expect(encryptionResult.atEncryptionMetaData, isNotNull); expect(encryptionResult.result, isNotEmpty); expect(encryptionResult.atEncryptionMetaData.encryptionKeyType, @@ -209,7 +210,7 @@ void main() { 'AESEncryptionAlgo'); expect(encryptionResult.atEncryptionMetaData.iv, iv); - final decryptionResult = atChops.decryptString( + final decryptionResult = await atChops.decryptString( encryptionResult.result, EncryptionKeyType.aes256, iv: iv); expect(decryptionResult.result, isNotEmpty); @@ -221,18 +222,18 @@ void main() { expect(decryptionResult.result, data); }); - test('validate decryption behaviour with null iv', () { + test('validate decryption behaviour with null iv', () async { final aesKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); final atChopsKeys = AtChopsKeys()..selfEncryptionKey = aesKey; final atChops = AtChopsImpl(atChopsKeys); var iv = AtChopsUtil.generateRandomIV(15); var utf8EncData = utf8.encode('abcd'); - var encryptionResult = - atChops.encryptBytes(utf8EncData, EncryptionKeyType.aes256, iv: iv); + var encryptionResult = await atChops + .encryptBytes(utf8EncData, EncryptionKeyType.aes256, iv: iv); expect( - () => atChops.decryptBytes( + () async => await atChops.decryptBytes( encryptionResult.result, EncryptionKeyType.aes256, iv: null), throwsA(predicate((e) => e is AtDecryptionException && diff --git a/packages/at_chops/test/pkcs7_padding_test.dart b/packages/at_chops/test/pkcs7_padding_test.dart new file mode 100644 index 00000000..bee04e5c --- /dev/null +++ b/packages/at_chops/test/pkcs7_padding_test.dart @@ -0,0 +1,82 @@ +import 'package:at_chops/src/algorithm/padding/padding_params.dart'; +import 'package:at_chops/src/algorithm/padding/pkcs7padding.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests to verify pkcs7 padding', () { + test('A test to verify padding when data length is less than block size', + () { + final paddingAlgo = PKCS7Padding(PaddingParams()..blockSize = 16); + var dataString = 'Hello World'; + var unPaddedData = dataString.codeUnits; + var paddedData = paddingAlgo.addPadding(dataString.codeUnits); + int padValue = 5; //expected padding value + expect(paddedData.last, padValue); + for (int i = 1; i <= padValue; i++) { + expect(paddedData[paddedData.length - i], padValue); + } + var dataAfterRemovingPadding = paddingAlgo.removePadding(paddedData); + expect(dataAfterRemovingPadding, unPaddedData); + }); + test('A test to verify padding when data length is equal to block size', + () { + final paddingAlgo = PKCS7Padding(PaddingParams()..blockSize = 16); + var dataString = 'Hello World12345'; + var unPaddedData = dataString.codeUnits; + var paddedData = paddingAlgo.addPadding(dataString.codeUnits); + int padValue = 16; //expected padding value + expect(paddedData.last, padValue); + for (int i = 1; i <= padValue; i++) { + expect(paddedData[paddedData.length - i], padValue); + } + var dataAfterRemovingPadding = paddingAlgo.removePadding(paddedData); + expect(dataAfterRemovingPadding, unPaddedData); + }); + test( + 'A test to verify padding when data length is one less than block size', + () { + final paddingAlgo = PKCS7Padding(PaddingParams()..blockSize = 16); + var dataString = 'Hello World1234'; + var unPaddedData = dataString.codeUnits; + var paddedData = paddingAlgo.addPadding(dataString.codeUnits); + int padValue = 1; //expected padding value + expect(paddedData.last, padValue); + for (int i = 1; i <= padValue; i++) { + expect(paddedData[paddedData.length - i], padValue); + } + var dataAfterRemovingPadding = paddingAlgo.removePadding(paddedData); + expect(dataAfterRemovingPadding, unPaddedData); + }); + test('A test to verify invalid block size', () { + var paddingAlgo = PKCS7Padding(PaddingParams()..blockSize = -10); + var dataString = 'Hello World1234'; + expect( + () => paddingAlgo.addPadding(dataString.codeUnits), + throwsA(predicate((e) => + e is AtEncryptionException && + e.toString().contains('Block size must be between 1 and 255.')))); + paddingAlgo = PKCS7Padding(PaddingParams()..blockSize = 0); + expect( + () => paddingAlgo.addPadding(dataString.codeUnits), + throwsA(predicate((e) => + e is AtEncryptionException && + e.toString().contains('Block size must be between 1 and 255.')))); + paddingAlgo = PKCS7Padding(PaddingParams()..blockSize = 300); + expect( + () => paddingAlgo.addPadding(dataString.codeUnits), + throwsA(predicate((e) => + e is AtEncryptionException && + e.toString().contains('Block size must be between 1 and 255.')))); + }); + test('A test to verify invalid input data to remove padding', () { + var paddingAlgo = PKCS7Padding(PaddingParams()); + List invalidInput = []; + expect( + () => paddingAlgo.removePadding(invalidInput), + throwsA(predicate((e) => + e is AtDecryptionException && + e.toString().contains('Encrypted data cannot be empty')))); + }); + }); +} diff --git a/tests/at_onboarding_cli_functional_tests/pubspec.yaml b/tests/at_onboarding_cli_functional_tests/pubspec.yaml index 1e6171f4..b430affb 100644 --- a/tests/at_onboarding_cli_functional_tests/pubspec.yaml +++ b/tests/at_onboarding_cli_functional_tests/pubspec.yaml @@ -14,13 +14,24 @@ dependencies: dependency_overrides: at_auth: - path: ../../packages/at_auth + git: + url: https://github.com/atsign-foundation/at_libraries.git + path: packages/at_auth + ref: at_auth_faster_aes_changes + at_client: + git: + url: https://github.com/atsign-foundation/at_client_sdk.git + path: packages/at_client + ref: uptake_atchops_better_crypto at_onboarding_cli: path: ../../packages/at_onboarding_cli at_commons: path: ../../packages/at_commons at_chops: - path: ../../packages/at_chops + git: + url: https://github.com/atsign-foundation/at_libraries.git + path: packages/at_chops + ref: at_chops_faster_aes at_cli_commons: path: ../../packages/at_cli_commons