From c8ff1149d73515172112c7e4149885c447651237 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 22 Feb 2023 20:16:40 +0100 Subject: [PATCH] feat(api)!: rework full api with better object oriented architecture --- packages/native_crypto/analysis_options.yaml | 2 +- packages/native_crypto/lib/native_crypto.dart | 30 +- .../native_crypto/lib/native_crypto_ext.dart | 11 - .../lib/src/builders/builders.dart | 14 +- .../lib/src/builders/decryption_builder.dart | 76 ++-- .../lib/src/builders/encryption_builder.dart | 61 +++ .../lib/src/ciphers/aes/aes.dart | 399 ++++++++++++------ .../lib/src/ciphers/aes/aes_cipher_chunk.dart | 67 +++ .../lib/src/ciphers/aes/aes_key_size.dart | 22 +- .../lib/src/ciphers/aes/aes_mode.dart | 36 +- .../lib/src/ciphers/aes/aes_padding.dart | 20 +- .../lib/src/ciphers/ciphers.dart | 13 +- .../lib/src/core/cipher_text.dart | 117 ----- .../lib/src/core/cipher_text_wrapper.dart | 191 --------- .../lib/src/core/constants/constants.dart | 13 + packages/native_crypto/lib/src/core/core.dart | 20 +- .../lib/src/core/enums/encoding.dart | 17 + .../lib/src/core/enums/enums.dart | 8 + .../lib/src/core/enums/hash_algorithm.dart | 15 + .../lib/src/core/extensions/extensions.dart | 10 + .../core/extensions/list_int_extension.dart | 12 + .../extensions/list_uint8_list_extension.dart | 19 + .../src/core/extensions/string_extension.dart | 36 ++ .../core/extensions/uint8_list_extension.dart | 69 +++ .../lib/src/core/utils/chunk_factory.dart | 12 + .../lib/src/core/utils/cipher_text.dart | 47 +++ .../lib/src/core/utils/platform.dart | 9 + .../lib/src/core/utils/utils.dart | 8 + .../native_crypto/lib/src/digest/digest.dart | 8 + .../native_crypto/lib/src/digest/hash.dart | 62 +++ .../native_crypto/lib/src/digest/hmac.dart | 67 +++ .../lib/src/domain/base_key.dart | 37 ++ .../{interfaces => domain}/byte_array.dart | 84 ++-- .../native_crypto/lib/src/domain/cipher.dart | 32 ++ .../lib/src/domain/cipher_chunk.dart | 31 ++ .../native_crypto/lib/src/domain/domain.dart | 14 + .../native_crypto/lib/src/domain/hash.dart | 24 ++ .../native_crypto/lib/src/domain/hmac.dart | 25 ++ .../src/domain/key_derivation_function.dart | 27 ++ .../native_crypto/lib/src/domain/random.dart | 18 + .../lib/src/interfaces/base_key.dart | 22 - .../lib/src/interfaces/builder.dart | 14 - .../lib/src/interfaces/cipher.dart | 53 --- .../lib/src/interfaces/interfaces.dart | 14 - .../lib/src/interfaces/keyderivation.dart | 23 - packages/native_crypto/lib/src/kdf/kdf.dart | 10 - .../native_crypto/lib/src/kdf/pbkdf2.dart | 176 ++++---- packages/native_crypto/lib/src/keys/keys.dart | 10 - .../lib/src/keys/secret_key.dart | 75 ++-- packages/native_crypto/lib/src/platform.dart | 12 - .../lib/src/random/secure_random.dart | 50 +++ .../lib/src/utils/cipher_algorithm.dart | 11 - .../native_crypto/lib/src/utils/encoding.dart | 10 - .../lib/src/utils/extensions.dart | 101 ----- .../lib/src/utils/hash_algorithm.dart | 51 --- .../lib/src/utils/kdf_algorithm.dart | 11 - packages/native_crypto/pubspec.yaml | 27 +- packages/native_crypto/pubspec_overrides.yaml | 8 + .../test/mocks/mock_native_crypto_api.dart | 193 +++++++++ .../mocks/mock_native_crypto_platform.dart | 192 --------- .../test/src/aes_cipher_test.dart | 323 -------------- .../test/src/cipher_text_test.dart | 192 --------- .../test/src/cipher_text_wrapper_test.dart | 349 --------------- .../native_crypto/test/src/digest_test.dart | 116 +++++ .../test/src/hash_algorithm_test.dart | 128 ------ .../native_crypto/test/src/pbkdf2_test.dart | 389 ++++++----------- .../native_crypto/test/src/random_test.dart | 63 +++ .../test/src/secret_key_test.dart | 115 +---- 68 files changed, 1861 insertions(+), 2660 deletions(-) delete mode 100644 packages/native_crypto/lib/native_crypto_ext.dart create mode 100644 packages/native_crypto/lib/src/builders/encryption_builder.dart create mode 100644 packages/native_crypto/lib/src/ciphers/aes/aes_cipher_chunk.dart delete mode 100644 packages/native_crypto/lib/src/core/cipher_text.dart delete mode 100644 packages/native_crypto/lib/src/core/cipher_text_wrapper.dart create mode 100644 packages/native_crypto/lib/src/core/constants/constants.dart create mode 100644 packages/native_crypto/lib/src/core/enums/encoding.dart create mode 100644 packages/native_crypto/lib/src/core/enums/enums.dart create mode 100644 packages/native_crypto/lib/src/core/enums/hash_algorithm.dart create mode 100644 packages/native_crypto/lib/src/core/extensions/extensions.dart create mode 100644 packages/native_crypto/lib/src/core/extensions/list_int_extension.dart create mode 100644 packages/native_crypto/lib/src/core/extensions/list_uint8_list_extension.dart create mode 100644 packages/native_crypto/lib/src/core/extensions/string_extension.dart create mode 100644 packages/native_crypto/lib/src/core/extensions/uint8_list_extension.dart create mode 100644 packages/native_crypto/lib/src/core/utils/chunk_factory.dart create mode 100644 packages/native_crypto/lib/src/core/utils/cipher_text.dart create mode 100644 packages/native_crypto/lib/src/core/utils/platform.dart create mode 100644 packages/native_crypto/lib/src/core/utils/utils.dart create mode 100644 packages/native_crypto/lib/src/digest/digest.dart create mode 100644 packages/native_crypto/lib/src/digest/hash.dart create mode 100644 packages/native_crypto/lib/src/digest/hmac.dart create mode 100644 packages/native_crypto/lib/src/domain/base_key.dart rename packages/native_crypto/lib/src/{interfaces => domain}/byte_array.dart (61%) create mode 100644 packages/native_crypto/lib/src/domain/cipher.dart create mode 100644 packages/native_crypto/lib/src/domain/cipher_chunk.dart create mode 100644 packages/native_crypto/lib/src/domain/domain.dart create mode 100644 packages/native_crypto/lib/src/domain/hash.dart create mode 100644 packages/native_crypto/lib/src/domain/hmac.dart create mode 100644 packages/native_crypto/lib/src/domain/key_derivation_function.dart create mode 100644 packages/native_crypto/lib/src/domain/random.dart delete mode 100644 packages/native_crypto/lib/src/interfaces/base_key.dart delete mode 100644 packages/native_crypto/lib/src/interfaces/builder.dart delete mode 100644 packages/native_crypto/lib/src/interfaces/cipher.dart delete mode 100644 packages/native_crypto/lib/src/interfaces/interfaces.dart delete mode 100644 packages/native_crypto/lib/src/interfaces/keyderivation.dart delete mode 100644 packages/native_crypto/lib/src/kdf/kdf.dart delete mode 100644 packages/native_crypto/lib/src/keys/keys.dart delete mode 100644 packages/native_crypto/lib/src/platform.dart create mode 100644 packages/native_crypto/lib/src/random/secure_random.dart delete mode 100644 packages/native_crypto/lib/src/utils/cipher_algorithm.dart delete mode 100644 packages/native_crypto/lib/src/utils/encoding.dart delete mode 100644 packages/native_crypto/lib/src/utils/extensions.dart delete mode 100644 packages/native_crypto/lib/src/utils/hash_algorithm.dart delete mode 100644 packages/native_crypto/lib/src/utils/kdf_algorithm.dart create mode 100644 packages/native_crypto/pubspec_overrides.yaml create mode 100644 packages/native_crypto/test/mocks/mock_native_crypto_api.dart delete mode 100644 packages/native_crypto/test/mocks/mock_native_crypto_platform.dart delete mode 100644 packages/native_crypto/test/src/aes_cipher_test.dart delete mode 100644 packages/native_crypto/test/src/cipher_text_test.dart delete mode 100644 packages/native_crypto/test/src/cipher_text_wrapper_test.dart create mode 100644 packages/native_crypto/test/src/digest_test.dart delete mode 100644 packages/native_crypto/test/src/hash_algorithm_test.dart create mode 100644 packages/native_crypto/test/src/random_test.dart diff --git a/packages/native_crypto/analysis_options.yaml b/packages/native_crypto/analysis_options.yaml index db48808..82177cd 100644 --- a/packages/native_crypto/analysis_options.yaml +++ b/packages/native_crypto/analysis_options.yaml @@ -1 +1 @@ -include: package:wyatt_analysis/analysis_options.flutter.experimental.yaml \ No newline at end of file +include: package:wyatt_analysis/analysis_options.flutter.yaml \ No newline at end of file diff --git a/packages/native_crypto/lib/native_crypto.dart b/packages/native_crypto/lib/native_crypto.dart index 2b294a4..2a7dd10 100644 --- a/packages/native_crypto/lib/native_crypto.dart +++ b/packages/native_crypto/lib/native_crypto.dart @@ -1,11 +1,8 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: native_crypto.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 26/05/2022 12:10:42 -// ----- -// Copyright (c) 2021 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. /// Fast and powerful cryptographic functions /// thanks to javax.crypto, CommonCrypto and CryptoKit. @@ -13,18 +10,13 @@ /// Author: Hugo Pointcheval library native_crypto; -export 'package:native_crypto_platform_interface/src/utils/exception.dart'; +export 'package:native_crypto_platform_interface/src/core/exceptions/exception.dart'; export 'src/builders/builders.dart'; export 'src/ciphers/ciphers.dart'; export 'src/core/core.dart'; -export 'src/interfaces/interfaces.dart'; -export 'src/kdf/kdf.dart'; -export 'src/keys/keys.dart'; -// Utils -export 'src/utils/cipher_algorithm.dart'; -export 'src/utils/hash_algorithm.dart'; -export 'src/utils/kdf_algorithm.dart'; - -// ignore: constant_identifier_names -const String AUTHOR = 'Hugo Pointcheval'; +export 'src/digest/digest.dart'; +export 'src/domain/domain.dart'; +export 'src/kdf/pbkdf2.dart'; +export 'src/keys/secret_key.dart'; +export 'src/random/secure_random.dart'; diff --git a/packages/native_crypto/lib/native_crypto_ext.dart b/packages/native_crypto/lib/native_crypto_ext.dart deleted file mode 100644 index 0e9502e..0000000 --- a/packages/native_crypto/lib/native_crypto_ext.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: native_crypto_ext.dart -// Created Date: 26/05/2022 19:36:54 -// Last Modified: 26/05/2022 19:38:44 -// ----- -// Copyright (c) 2022 - -export 'src/utils/encoding.dart'; -export 'src/utils/extensions.dart'; diff --git a/packages/native_crypto/lib/src/builders/builders.dart b/packages/native_crypto/lib/src/builders/builders.dart index d846197..df13d09 100644 --- a/packages/native_crypto/lib/src/builders/builders.dart +++ b/packages/native_crypto/lib/src/builders/builders.dart @@ -1,10 +1,8 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: builders.dart -// Created Date: 23/05/2022 22:56:03 -// Last Modified: 26/05/2022 19:22:19 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. export 'decryption_builder.dart'; +export 'encryption_builder.dart'; diff --git a/packages/native_crypto/lib/src/builders/decryption_builder.dart b/packages/native_crypto/lib/src/builders/decryption_builder.dart index 998fdcf..c9d6dbe 100644 --- a/packages/native_crypto/lib/src/builders/decryption_builder.dart +++ b/packages/native_crypto/lib/src/builders/decryption_builder.dart @@ -1,46 +1,60 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: decryption_builder.dart -// Created Date: 26/05/2022 19:07:52 -// Last Modified: 26/05/2022 19:21:00 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; -import 'package:native_crypto/src/interfaces/cipher.dart'; +import 'package:native_crypto/src/core/utils/cipher_text.dart'; -class DecryptionBuilder extends StatelessWidget { - final Cipher cipher; - final CipherTextWrapper data; - final Widget Function(BuildContext context) onLoading; - final Widget Function(BuildContext context, Object error) onError; - final Widget Function(BuildContext context, Uint8List plainText) onSuccess; +import 'package:native_crypto/src/domain/cipher.dart'; +import 'package:native_crypto/src/domain/cipher_chunk.dart'; +/// {@template decryption_builder} +/// A [StatelessWidget] that builds a [FutureBuilder] that will decrypt a +/// [CipherText] using a [Cipher]. +/// {@endtemplate} +class DecryptionBuilder extends StatelessWidget { + /// {@macro decryption_builder} const DecryptionBuilder({ - super.key, required this.cipher, - required this.data, + required this.cipherText, required this.onLoading, required this.onError, required this.onSuccess, + super.key, }); + /// The [Cipher] that will be used to decrypt the [CipherText]. + final Cipher cipher; + + /// The [CipherText] that will be decrypted. + final CipherText cipherText; + + /// The [Widget] that will be displayed while the [CipherText] is being + /// decrypted. + final Widget Function(BuildContext context) onLoading; + + /// The [Widget] that will be displayed if an error occurs while decrypting + /// the [CipherText]. + final Widget Function(BuildContext context, Object error) onError; + + /// The [Widget] that will be displayed once the [CipherText] has been + /// decrypted. + final Widget Function(BuildContext context, Uint8List plainText) onSuccess; + @override - Widget build(BuildContext context) { - return FutureBuilder( - future: cipher.decrypt(data), - builder: (context, snapshot) { - if (snapshot.hasData) { - return onSuccess(context, snapshot.data!); - } else if (snapshot.hasError) { - return onError(context, snapshot.error!); - } - return onLoading(context); - }, - ); - } + Widget build(BuildContext context) => FutureBuilder( + future: cipher.decrypt(cipherText), + builder: (context, snapshot) { + if (snapshot.hasData) { + return onSuccess(context, snapshot.data!); + } else if (snapshot.hasError) { + return onError(context, snapshot.error!); + } + return onLoading(context); + }, + ); } diff --git a/packages/native_crypto/lib/src/builders/encryption_builder.dart b/packages/native_crypto/lib/src/builders/encryption_builder.dart new file mode 100644 index 0000000..5782730 --- /dev/null +++ b/packages/native_crypto/lib/src/builders/encryption_builder.dart @@ -0,0 +1,61 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:native_crypto/src/core/utils/cipher_text.dart'; + +import 'package:native_crypto/src/domain/cipher.dart'; +import 'package:native_crypto/src/domain/cipher_chunk.dart'; + +/// {@template encryption_builder} +/// A [StatelessWidget] that builds a [FutureBuilder] that will +/// encrypt a [Uint8List] using a [Cipher]. +/// {@endtemplate} +class EncryptionBuilder extends StatelessWidget { + /// {@macro encryption_builder} + const EncryptionBuilder({ + required this.cipher, + required this.plainText, + required this.onLoading, + required this.onError, + required this.onSuccess, + super.key, + }); + + /// The [Cipher] that will be used to encrypt the [Uint8List]. + final Cipher cipher; + + /// The [Uint8List] that will be encrypted. + final Uint8List plainText; + + /// The [Widget] that will be displayed while the [Uint8List] is being + /// encrypted. + final Widget Function(BuildContext context) onLoading; + + /// The [Widget] that will be displayed if an error occurs while encrypting + /// the [Uint8List]. + final Widget Function(BuildContext context, Object error) onError; + + /// The [Widget] that will be displayed once the [Uint8List] has been + /// encrypted. + final Widget Function(BuildContext context, CipherText cipherText) + onSuccess; + + @override + Widget build(BuildContext context) => FutureBuilder>( + future: cipher.encrypt(plainText), + builder: (context, snapshot) { + if (snapshot.hasData) { + return onSuccess(context, snapshot.data!); + } else if (snapshot.hasError) { + return onError(context, snapshot.error!); + } + return onLoading(context); + }, + ); +} diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index b1113fc..8eda12e 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -1,181 +1,300 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: aes.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 27/05/2022 12:13:28 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. +import 'dart:io'; import 'dart:typed_data'; +import 'package:native_crypto/src/ciphers/aes/aes_cipher_chunk.dart'; import 'package:native_crypto/src/ciphers/aes/aes_key_size.dart'; import 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; -import 'package:native_crypto/src/core/cipher_text.dart'; -import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; -import 'package:native_crypto/src/interfaces/cipher.dart'; +import 'package:native_crypto/src/core/constants/constants.dart'; +import 'package:native_crypto/src/core/extensions/uint8_list_extension.dart'; +import 'package:native_crypto/src/core/utils/cipher_text.dart'; +import 'package:native_crypto/src/core/utils/platform.dart'; +import 'package:native_crypto/src/domain/cipher.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; -import 'package:native_crypto/src/platform.dart'; -import 'package:native_crypto/src/utils/cipher_algorithm.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; +export 'aes_cipher_chunk.dart'; export 'aes_key_size.dart'; export 'aes_mode.dart'; export 'aes_padding.dart'; -/// An AES cipher. +/// {@template aes} +/// AES cipher. /// -/// [AES] is a [Cipher] that can be used to encrypt or decrypt data. -class AES implements Cipher { - final SecretKey _key; +/// [AES] is a symmetric cipher which means that the same key is used to encrypt +/// and decrypt the data. +/// {@endtemplate} +class AES implements Cipher { + const AES({ + required this.key, + required this.mode, + required this.padding, + this.chunkSize = Constants.defaultChunkSize, + }); + + static const String _algorithm = 'aes'; + + /// The key used to encrypt and decrypt the data. + final SecretKey key; + + /// The [AESMode] used by this [AES]. final AESMode mode; + + /// The [AESPadding] used by this [AES]. final AESPadding padding; - @override - CipherAlgorithm get algorithm => CipherAlgorithm.aes; + /// The size of the cipher text chunks. + final int chunkSize; - AES(SecretKey key, [this.mode = AESMode.gcm, this.padding = AESPadding.none]) - : _key = key { - if (!AESKeySize.supportedSizes.contains(key.bitLength)) { - throw NativeCryptoException( - message: 'Invalid key size! ' - 'Expected: ${AESKeySize.supportedSizes.join(', ')} bits', - code: NativeCryptoExceptionCode.invalid_key_length.code, + @override + Future decrypt(CipherText cipherText) async { + final BytesBuilder plainText = BytesBuilder(copy: false); + final chunks = cipherText.chunks; + + int i = 0; + for (final chunk in chunks) { + plainText.add(await _decryptChunk(chunk.bytes, count: i++)); + } + + return plainText.toBytes(); + } + + @override + Future decryptFile(File cipherTextFile, File plainTextFile) { + if (!cipherTextFile.existsSync()) { + throw ArgumentError.value( + cipherTextFile.path, + 'cipherTextFile.path', + 'File does not exist!', + ); + } + + if (plainTextFile.existsSync()) { + throw ArgumentError.value( + plainTextFile.path, + 'plainTextFile.path', + 'File already exists!', + ); + } + + return platform.decryptFile( + cipherTextPath: cipherTextFile.path, + plainTextPath: plainTextFile.path, + key: key.bytes, + algorithm: _algorithm, + ); + } + + @override + Future> encrypt(Uint8List plainText) async { + final chunks = []; + final chunkedPlainText = plainText.chunked(chunkSize); + + int i = 0; + for (final plainTextChunk in chunkedPlainText) { + final bytes = await _encryptChunk(plainTextChunk, count: i++); + chunks.add( + AESCipherChunk( + bytes, + ivLength: mode.ivLength, + tagLength: mode.tagLength, + ), + ); + } + + return CipherText.fromChunks( + chunks, + chunkFactory: (bytes) => AESCipherChunk( + bytes, + ivLength: mode.ivLength, + tagLength: mode.tagLength, + ), + chunkSize: chunkSize, + ); + } + + @override + Future encryptFile(File plainTextFile, File cipherTextFile) { + if (!plainTextFile.existsSync()) { + throw ArgumentError.value( + plainTextFile.path, + 'plainTextFile.path', + 'File does not exist!', + ); + } + + if (cipherTextFile.existsSync()) { + throw ArgumentError.value( + cipherTextFile.path, + 'cipherTextFile.path', + 'File already exists!', + ); + } + + return platform.encryptFile( + plainTextPath: plainTextFile.path, + cipherTextPath: cipherTextFile.path, + key: key.bytes, + algorithm: _algorithm, + ); + } + + /// Encrypts the [plainText] with the [iv] chosen by the Flutter side. + /// + /// Prefer using [encrypt] instead which will generate a + /// random [iv] on the native side. + /// + /// Note: this method doesn't chunk the data. It can lead to memory issues + /// if the [plainText] is too big. Use [encrypt] instead. + Future encryptWithIV( + Uint8List plainText, + Uint8List iv, + ) async { + // Check if the cipher is correctly initialized + _isCorrectlyInitialized(); + + if (iv.length != mode.ivLength) { + throw ArgumentError.value( + iv.length, + 'iv.length', + 'Invalid iv length! ' + 'Expected: ${mode.ivLength}', + ); + } + + final bytes = await platform.encryptWithIV( + plainText: plainText, + iv: iv, + key: key.bytes, + algorithm: _algorithm, + ); + + // TODO(hpcl): move these checks to the platform interface + if (bytes == null) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes', + ); + } + + if (bytes.isEmpty) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned no data', + ); + } + + return AESCipherChunk( + bytes, + ivLength: mode.ivLength, + tagLength: mode.tagLength, + ); + } + + /// Ensures that the cipher is correctly initialized. + bool _isCorrectlyInitialized() { + final keySize = key.length * 8; + if (!AESKeySize.supportedSizes.contains(keySize)) { + throw ArgumentError.value( + keySize, + 'keySize', + 'Invalid key size! ' + 'Expected: ${AESKeySize.supportedSizes.join(', ')}', ); } if (!mode.supportedPaddings.contains(padding)) { - throw NativeCryptoException( - message: 'Invalid padding! ' + throw ArgumentError.value( + padding, + 'padding', + 'Invalid padding! ' 'Expected: ${mode.supportedPaddings.join(', ')}', - code: NativeCryptoExceptionCode.invalid_padding.code, ); } + + return true; } - Future _decrypt( - CipherText cipherText, { - int chunkCount = 0, + /// Encrypts the plain text chunk. + Future _encryptChunk(Uint8List plainChunk, {int count = 0}) async { + // Check if the cipher is correctly initialized + _isCorrectlyInitialized(); + + Uint8List? bytes; + + try { + bytes = await platform.encrypt( + plainChunk, + key: key.bytes, + algorithm: _algorithm, + ); + } on NativeCryptoException catch (e) { + throw e.copyWith( + message: 'Failed to encrypt chunk #$count: ${e.message}', + ); + } + + // TODO(hpcl): move these checks to the platform interface + if (bytes == null) { + throw NativeCryptoException( + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes on chunk #$count', + ); + } + + if (bytes.isEmpty) { + throw NativeCryptoException( + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned no data on chunk #$count', + ); + } + + return bytes; + } + + /// Decrypts the cipher text chunk. + Future _decryptChunk( + Uint8List cipherChunk, { + int count = 0, }) async { - Uint8List? decrypted; + // Check if the cipher is correctly initialized + _isCorrectlyInitialized(); + + Uint8List? bytes; try { - decrypted = await platform.decrypt( - cipherText.bytes, - _key.bytes, - algorithm.name, + bytes = await platform.decrypt( + cipherChunk, + key: key.bytes, + algorithm: _algorithm, ); - } catch (e, s) { + } on NativeCryptoException catch (e) { + throw e.copyWith( + message: 'Failed to decrypt chunk #$count: ${e.message}', + ); + } + + // TODO(hpcl): move these checks to the platform interface + if (bytes == null) { throw NativeCryptoException( - message: '$e', - code: NativeCryptoExceptionCode.platform_throws.code, - stackTrace: s, + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes on chunk #$count', ); } - if (decrypted.isNull) { + if (bytes.isEmpty) { throw NativeCryptoException( - message: 'Platform returned null when decrypting chunk #$chunkCount', - code: NativeCryptoExceptionCode.platform_returned_null.code, - ); - } else if (decrypted!.isEmpty) { - throw NativeCryptoException( - message: 'Platform returned no data when decrypting chunk #$chunkCount', - code: NativeCryptoExceptionCode.platform_returned_empty_data.code, - ); - } else { - return decrypted; - } - } - - Future _encrypt(Uint8List data, {int chunkCount = 0}) async { - Uint8List? encrypted; - - try { - encrypted = await platform.encrypt( - data, - _key.bytes, - algorithm.name, - ); - } catch (e, s) { - throw NativeCryptoException( - message: '$e on chunk #$chunkCount', - code: NativeCryptoExceptionCode.platform_throws.code, - stackTrace: s, + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned no data on chunk #$count', ); } - if (encrypted.isNull) { - throw NativeCryptoException( - message: 'Platform returned null when encrypting chunk #$chunkCount', - code: NativeCryptoExceptionCode.platform_returned_null.code, - ); - } else if (encrypted!.isEmpty) { - throw NativeCryptoException( - message: 'Platform returned no data when encrypting chunk #$chunkCount', - code: NativeCryptoExceptionCode.platform_returned_empty_data.code, - ); - } else { - try { - return CipherText.fromBytes( - encrypted, - ivLength: 12, - messageLength: encrypted.length - 28, - tagLength: 16, - cipherAlgorithm: CipherAlgorithm.aes, - ); - } on NativeCryptoException catch (e, s) { - throw NativeCryptoException( - message: '${e.message} on chunk #$chunkCount', - code: e.code, - stackTrace: s, - ); - } - } - } - - @override - Future decrypt(CipherTextWrapper cipherText) async { - final BytesBuilder decryptedData = BytesBuilder(copy: false); - - if (cipherText.isList) { - int chunkCount = 0; - for (final CipherText chunk in cipherText.list) { - decryptedData.add(await _decrypt(chunk, chunkCount: chunkCount++)); - } - } else { - decryptedData.add(await _decrypt(cipherText.single)); - } - - return decryptedData.toBytes(); - } - - @override - Future encrypt(Uint8List data) async { - if (data.isEmpty) { - return CipherTextWrapper.empty(); - } - CipherTextWrapper cipherTextWrapper; - Uint8List dataToEncrypt; - final int chunkNb = (data.length / Cipher.bytesCountPerChunk).ceil(); - - if (chunkNb > 1) { - cipherTextWrapper = CipherTextWrapper.empty(); - for (var i = 0; i < chunkNb; i++) { - dataToEncrypt = i < (chunkNb - 1) - ? data.sublist( - i * Cipher.bytesCountPerChunk, - (i + 1) * Cipher.bytesCountPerChunk, - ) - : data.sublist(i * Cipher.bytesCountPerChunk); - cipherTextWrapper.add(await _encrypt(dataToEncrypt, chunkCount: i)); - } - } else { - cipherTextWrapper = CipherTextWrapper.single(await _encrypt(data)); - } - - return cipherTextWrapper; + return bytes; } } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_cipher_chunk.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_cipher_chunk.dart new file mode 100644 index 0000000..7f8bb45 --- /dev/null +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_cipher_chunk.dart @@ -0,0 +1,67 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/domain/cipher_chunk.dart'; + +class AESCipherChunk extends CipherChunk { + const AESCipherChunk( + super.bytes, { + required this.ivLength, + required this.tagLength, + }); + + /// Creates a [AESCipherChunk] from a [List]. + AESCipherChunk.fromList( + super.list, { + required this.ivLength, + required this.tagLength, + }) : super.fromList(); + + /// Creates a [AESCipherChunk] from a [String] encoded in UTF-8. + AESCipherChunk.fromUtf8( + super.encoded, { + required this.ivLength, + required this.tagLength, + }) : super.fromUtf8(); + + /// Creates a [AESCipherChunk] from a [String] encoded in UTF-16. + AESCipherChunk.fromUtf16( + super.encoded, { + required this.ivLength, + required this.tagLength, + }) : super.fromUtf16(); + + /// Creates a [AESCipherChunk] from a [String] encoded in Hexadecimal. + AESCipherChunk.fromBase16( + super.encoded, { + required this.ivLength, + required this.tagLength, + }) : super.fromBase16(); + + /// Creates a [AESCipherChunk] from a [String] encoded in Base64. + AESCipherChunk.fromBase64( + super.encoded, { + required this.ivLength, + required this.tagLength, + }) : super.fromBase64(); + + /// Intialization vector length. + final int ivLength; + + /// Tag length. + final int tagLength; + + /// Returns the initialization vector, or nonce of the [AESCipherChunk]. + Uint8List get iv => bytes.sublist(0, ivLength); + + /// Returns the tag of the [AESCipherChunk]. + Uint8List get tag => bytes.sublist(bytes.length - tagLength, bytes.length); + + /// Returns the message of the [AESCipherChunk]. + Uint8List get message => bytes.sublist(ivLength, bytes.length - tagLength); +} diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart index befa22f..7bc8f26 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart @@ -1,18 +1,20 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: aes_key_size.dart -// Created Date: 23/05/2022 22:10:07 -// Last Modified: 26/05/2022 18:45:01 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. -/// Defines all available key sizes. +/// {@template aes_key_size} +/// Defines the key size of an AES cipher. +/// {@endtemplate} enum AESKeySize { bits128(128), bits192(192), bits256(256); + /// {@macro aes_key_size} + const AESKeySize(this.bits); + /// Returns the number of bits supported by an [AESKeySize]. static final List supportedSizes = [128, 192, 256]; @@ -21,6 +23,4 @@ enum AESKeySize { /// Returns the number of bytes in this [AESKeySize]. int get bytes => bits ~/ 8; - - const AESKeySize(this.bits); } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart index 4bbc7c4..9e34304 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart @@ -1,17 +1,29 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: aes_mode.dart -// Created Date: 23/05/2022 22:09:16 -// Last Modified: 26/05/2022 21:03:26 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; +import 'package:native_crypto/src/core/constants/constants.dart'; +/// {@template aes_mode} /// Defines the AES modes of operation. +/// {@endtemplate} enum AESMode { - gcm([AESPadding.none], 12, 16); + /// GCM mode. + gcm( + [AESPadding.none], + Constants.aesGcmNonceLength, + Constants.aesGcmTagLength, + ); + + /// {@macro aes_mode} + const AESMode( + this.supportedPaddings, [ + this.ivLength = 16, + this.tagLength = 0, + ]); /// Returns the list of supported [AESPadding] for this [AESMode]. final List supportedPaddings; @@ -21,10 +33,4 @@ enum AESMode { /// Returns the default tag length for this [AESMode]. final int tagLength; - - const AESMode( - this.supportedPaddings, [ - this.ivLength = 16, - this.tagLength = 0, - ]); } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart index 343ae03..0d26a9f 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart @@ -1,11 +1,11 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: aes_padding.dart -// Created Date: 23/05/2022 22:10:17 -// Last Modified: 25/05/2022 09:23:49 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. -/// Represents different paddings. -enum AESPadding { none } +/// Padding used for AES encryption. +enum AESPadding { + /// No padding. + none, +} diff --git a/packages/native_crypto/lib/src/ciphers/ciphers.dart b/packages/native_crypto/lib/src/ciphers/ciphers.dart index edae6a4..2b87da6 100644 --- a/packages/native_crypto/lib/src/ciphers/ciphers.dart +++ b/packages/native_crypto/lib/src/ciphers/ciphers.dart @@ -1,10 +1,7 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: ciphers.dart -// Created Date: 23/05/2022 22:56:30 -// Last Modified: 23/05/2022 22:56:47 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. export 'aes/aes.dart'; diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart deleted file mode 100644 index 6accd59..0000000 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ /dev/null @@ -1,117 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher_text.dart -// Created Date: 16/12/2021 16:59:53 -// Last Modified: 27/05/2022 12:09:47 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; -import 'package:native_crypto/src/interfaces/byte_array.dart'; -import 'package:native_crypto/src/interfaces/cipher.dart'; -import 'package:native_crypto/src/utils/cipher_algorithm.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -/// Represents a cipher text in NativeCrypto. -/// -/// [CipherText] is a [ByteArray] that can be used to store encrypted data. -/// It is represented like: -/// ```txt -/// [IV + MESSAGE + TAG] -/// ``` -/// where: -/// - IV's length is [CipherText.ivLength] bytes. -/// - MESSAGE's length is [CipherText.messageLength] bytes. -/// - TAG's length is [CipherText.tagLength] bytes. -/// -/// Check [CipherTextWrapper] for more information. -class CipherText extends ByteArray { - final int _ivLength; - final int _messageLength; - final int _tagLength; - - final CipherAlgorithm? _cipherAlgorithm; - - const CipherText._( - this._ivLength, - this._messageLength, - this._tagLength, - this._cipherAlgorithm, - super.bytes, - ); - - factory CipherText.fromBytes( - Uint8List bytes, { - required int ivLength, - required int tagLength, - int? messageLength, - CipherAlgorithm? cipherAlgorithm, - }) { - messageLength ??= bytes.length - ivLength - tagLength; - - if (ivLength.isNegative || - messageLength.isNegative || - tagLength.isNegative) { - throw NativeCryptoException( - message: 'Invalid length! Must be positive.', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); - } - - if (bytes.isEmpty) { - throw NativeCryptoException( - message: 'Passed data is empty!', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); - } - - if (bytes.length != ivLength + messageLength + tagLength) { - throw NativeCryptoException( - message: 'Invalid cipher text length! ' - 'Expected: ${ivLength + messageLength + tagLength} bytes ' - 'got: ${bytes.length} bytes.', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); - } - - if (messageLength > Cipher.bytesCountPerChunk) { - throw NativeCryptoException( - message: 'Cipher text is too big! Consider using chunks.', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); - } - - return CipherText._( - ivLength, - messageLength, - tagLength, - cipherAlgorithm, - bytes, - ); - } - - /// Gets the [CipherAlgorithm] used to encrypt the [CipherText]. - CipherAlgorithm get cipherAlgorithm { - if (_cipherAlgorithm.isNotNull) { - return _cipherAlgorithm!; - } else { - throw NativeCryptoException( - message: 'Cipher algorithm is not specified', - code: NativeCryptoExceptionCode.invalid_cipher.code, - ); - } - } - - /// Gets the length of the [CipherText]'s IV. - int get ivLength => _ivLength; - - /// Gets the length of the [CipherText]'s Message. - int get messageLength => _messageLength; - - /// Gets the length of the [CipherText]'s Tag. - int get tagLength => _tagLength; -} diff --git a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart deleted file mode 100644 index 75a9dae..0000000 --- a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart +++ /dev/null @@ -1,191 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher_text_wrapper.dart -// Created Date: 26/05/2022 14:27:32 -// Last Modified: 27/05/2022 13:43:29 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:native_crypto/native_crypto.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; - -/// Wrapper for [CipherText] -/// -/// Typically, this object is the result of an encryption operation. -/// For decryption you have to build this before using it. -class CipherTextWrapper { - final CipherText? _single; - final List? _list; - - CipherTextWrapper._(this._single, this._list); - - /// Creates a [CipherTextWrapper] from a [CipherText]. - factory CipherTextWrapper.single(CipherText cipherText) => - CipherTextWrapper._(cipherText, null); - - /// Creates a [CipherTextWrapper] from a [List] of [CipherText]. - factory CipherTextWrapper.list(List cipherTexts) => - CipherTextWrapper._(null, cipherTexts); - - /// Creates an empty [List] in a [CipherTextWrapper]. - /// - /// This is useful when you want to create a [CipherTextWrapper] then - /// fill it with data. - factory CipherTextWrapper.empty() => CipherTextWrapper._(null, []); - - /// Creates a [CipherTextWrapper] from a [Uint8List]. - /// - /// This is a convenience method to create a [CipherTextWrapper] - /// from a [Uint8List]. It tries to detect if the [Uint8List] is a - /// single [CipherText] or a list of [CipherText]. - /// - /// You can customize the chunk size by passing a [chunkSize] parameter. - /// The default chunk size is [Cipher.bytesCountPerChunk]. - /// - /// Throw an [NativeCryptoExceptionCode] with - /// [NativeCryptoExceptionCode.invalid_argument] if the [Uint8List] is - /// not a valid [CipherText] or a [List] of [CipherText]. - factory CipherTextWrapper.fromBytes( - Uint8List bytes, { - required int ivLength, - required int tagLength, - CipherAlgorithm? cipherAlgorithm, - int? chunkSize, - }) { - chunkSize ??= Cipher.bytesCountPerChunk; - Cipher.bytesCountPerChunk = chunkSize; - - final int messageLength = bytes.length - ivLength - tagLength; - - if (messageLength <= chunkSize) { - return CipherTextWrapper.single( - CipherText.fromBytes( - bytes, - ivLength: ivLength, - tagLength: tagLength, - cipherAlgorithm: cipherAlgorithm, - ), - ); - } else { - final cipherTexts = []; - for (var i = 0; i < bytes.length; i += chunkSize + ivLength + tagLength) { - final chunk = bytes.trySublist(i, i + chunkSize + ivLength + tagLength); - - try { - cipherTexts.add( - CipherText.fromBytes( - chunk, - ivLength: ivLength, - tagLength: tagLength, - cipherAlgorithm: cipherAlgorithm, - ), - ); - } on NativeCryptoException catch (e, s) { - throw NativeCryptoException( - message: '${e.message} on chunk #$i', - code: e.code, - stackTrace: s, - ); - } - } - return CipherTextWrapper.list(cipherTexts); - } - } - - /// Checks if the [CipherText] is a single [CipherText]. - bool get isSingle => _single.isNotNull; - - /// Checks if the [CipherText] is a [List] of [CipherText]. - bool get isList => _list.isNotNull; - - /// Gets the [CipherText] if it's a single one. - /// - /// Throws [NativeCryptoException] with - /// [NativeCryptoExceptionCode.invalid_data] if it's not a single one. - CipherText get single { - if (isSingle) { - return _single!; - } else { - throw NativeCryptoException( - message: 'CipherTextWrapper is not single', - code: NativeCryptoExceptionCode.invalid_data.code, - ); - } - } - - /// Gets the [List] of [CipherText] if it's a list. - /// - /// Throws [NativeCryptoException] with - /// [NativeCryptoExceptionCode.invalid_data] if it's not a list. - List get list { - if (isList) { - return _list!; - } else { - throw NativeCryptoException( - message: 'CipherTextWrapper is not list', - code: NativeCryptoExceptionCode.invalid_data.code, - ); - } - } - - /// Gets the raw [Uint8List] of the [CipherText] or [List] of [CipherText]. - Uint8List get bytes { - if (isSingle) { - return single.bytes; - } else { - return list.map((cipherText) => cipherText.bytes).toList().combine(); - } - } - - /// Gets the number of parts of the [CipherText] or [List] of [CipherText]. - /// - /// Check [Cipher.bytesCountPerChunk] for more information. - int get chunkCount { - _single.isNull; - if (_single.isNotNull) { - return 1; - } else { - return _list?.length ?? 0; - } - } - - /// Gets the [CipherText] or the [List] of [CipherText]. - /// - /// Throws [NativeCryptoException] with - /// [NativeCryptoExceptionCode.invalid_data] if it's not a single or a list or - /// if [T] is not [CipherText] or [List] of [CipherText]. - T unwrap() { - if (isSingle && T == CipherText) { - return single as T; - } else if (isList && T == List) { - return list as T; - } else { - final String type = - isSingle ? 'CipherText' : (isList ? 'List' : 'unknown'); - throw NativeCryptoException( - message: 'CipherTextWrapper is not a $T but a $type, ' - 'you should use unwrap<$type>()', - code: NativeCryptoExceptionCode.invalid_data.code, - ); - } - } - - void add(CipherText cipherText) { - if (isSingle) { - throw NativeCryptoException( - message: 'CipherTextWrapper is already single', - code: NativeCryptoExceptionCode.invalid_data.code, - ); - } else if (isList) { - _list!.add(cipherText); - } else { - throw NativeCryptoException( - message: 'CipherTextWrapper is not single or list', - code: NativeCryptoExceptionCode.invalid_data.code, - ); - } - } -} diff --git a/packages/native_crypto/lib/src/core/constants/constants.dart b/packages/native_crypto/lib/src/core/constants/constants.dart new file mode 100644 index 0000000..c9b13ae --- /dev/null +++ b/packages/native_crypto/lib/src/core/constants/constants.dart @@ -0,0 +1,13 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +abstract class Constants { + /// The default chunk size in bytes used for encryption and decryption. + static const int defaultChunkSize = 33554432; + + static const int aesGcmNonceLength = 12; + static const int aesGcmTagLength = 16; +} diff --git a/packages/native_crypto/lib/src/core/core.dart b/packages/native_crypto/lib/src/core/core.dart index 32ad783..7a26b2a 100644 --- a/packages/native_crypto/lib/src/core/core.dart +++ b/packages/native_crypto/lib/src/core/core.dart @@ -1,11 +1,11 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: core.dart -// Created Date: 23/05/2022 23:05:26 -// Last Modified: 26/05/2022 17:10:25 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. -export 'cipher_text.dart'; -export 'cipher_text_wrapper.dart'; +export './constants/constants.dart'; +export './enums/enums.dart'; +export './extensions/extensions.dart'; +export './utils/utils.dart'; +export 'utils/platform.dart'; diff --git a/packages/native_crypto/lib/src/core/enums/encoding.dart b/packages/native_crypto/lib/src/core/enums/encoding.dart new file mode 100644 index 0000000..2b69508 --- /dev/null +++ b/packages/native_crypto/lib/src/core/enums/encoding.dart @@ -0,0 +1,17 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +/// An encoding used to convert a byte array to a string and vice-versa. +enum Encoding { + /// UTF-8 encoding, as defined by the Unicode standard. + utf8, + /// UTF-16 encoding, as defined by the Unicode standard. + utf16, + /// Base64 encoding, as defined by RFC 4648. + base64, + /// Hexadecimal encoding. + base16, +} diff --git a/packages/native_crypto/lib/src/core/enums/enums.dart b/packages/native_crypto/lib/src/core/enums/enums.dart new file mode 100644 index 0000000..bc34b6b --- /dev/null +++ b/packages/native_crypto/lib/src/core/enums/enums.dart @@ -0,0 +1,8 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +export 'encoding.dart'; +export 'hash_algorithm.dart'; diff --git a/packages/native_crypto/lib/src/core/enums/hash_algorithm.dart b/packages/native_crypto/lib/src/core/enums/hash_algorithm.dart new file mode 100644 index 0000000..7748efb --- /dev/null +++ b/packages/native_crypto/lib/src/core/enums/hash_algorithm.dart @@ -0,0 +1,15 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +/// The hash algorithm to use in Message Digest and HMAC. +enum HashAlgorithm { + /// The SHA-256 hash algorithm. + sha256, + /// The SHA-384 hash algorithm. + sha384, + /// The SHA-512 hash algorithm. + sha512, +} diff --git a/packages/native_crypto/lib/src/core/extensions/extensions.dart b/packages/native_crypto/lib/src/core/extensions/extensions.dart new file mode 100644 index 0000000..f10b73b --- /dev/null +++ b/packages/native_crypto/lib/src/core/extensions/extensions.dart @@ -0,0 +1,10 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +export 'list_int_extension.dart'; +export 'list_uint8_list_extension.dart'; +export 'string_extension.dart'; +export 'uint8_list_extension.dart'; diff --git a/packages/native_crypto/lib/src/core/extensions/list_int_extension.dart b/packages/native_crypto/lib/src/core/extensions/list_int_extension.dart new file mode 100644 index 0000000..5ae7252 --- /dev/null +++ b/packages/native_crypto/lib/src/core/extensions/list_int_extension.dart @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +extension ListIntExtension on List { + /// Converts a [List] of int to a [Uint8List]. + Uint8List toTypedList() => Uint8List.fromList(this); +} diff --git a/packages/native_crypto/lib/src/core/extensions/list_uint8_list_extension.dart b/packages/native_crypto/lib/src/core/extensions/list_uint8_list_extension.dart new file mode 100644 index 0000000..78dfe75 --- /dev/null +++ b/packages/native_crypto/lib/src/core/extensions/list_uint8_list_extension.dart @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/extensions/uint8_list_extension.dart'; + +extension ListUint8ListExtension on List { + /// Reduce a [List] of [Uint8List] to a [Uint8List]. + Uint8List combine() { + if (isEmpty) { + return Uint8List(0); + } + return reduce((value, element) => value | element); + } +} diff --git a/packages/native_crypto/lib/src/core/extensions/string_extension.dart b/packages/native_crypto/lib/src/core/extensions/string_extension.dart new file mode 100644 index 0000000..3a342b7 --- /dev/null +++ b/packages/native_crypto/lib/src/core/extensions/string_extension.dart @@ -0,0 +1,36 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/enums/encoding.dart'; +import 'package:native_crypto/src/core/extensions/list_int_extension.dart'; + +extension StringExtension on String { + /// Converts a [String] to a [Uint8List] using the specified [Encoding]. + Uint8List toBytes({Encoding from = Encoding.utf16}) { + Uint8List bytes; + switch (from) { + case Encoding.utf8: + bytes = utf8.encode(this).toTypedList(); + break; + case Encoding.utf16: + bytes = runes.toList().toTypedList(); + break; + case Encoding.base64: + bytes = base64.decode(this); + break; + case Encoding.base16: + assert(length.isEven, 'String needs to be an even length.'); + bytes = List.generate( + length ~/ 2, + (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), + ).toList().toTypedList(); + } + return bytes; + } +} diff --git a/packages/native_crypto/lib/src/core/extensions/uint8_list_extension.dart b/packages/native_crypto/lib/src/core/extensions/uint8_list_extension.dart new file mode 100644 index 0000000..daf870a --- /dev/null +++ b/packages/native_crypto/lib/src/core/extensions/uint8_list_extension.dart @@ -0,0 +1,69 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/enums/encoding.dart'; +import 'package:native_crypto/src/core/extensions/list_int_extension.dart'; + +extension Uint8ListExtension on Uint8List { + /// Returns a concatenation of this with the other [Uint8List]. + Uint8List plus(Uint8List other) => [...this, ...other].toTypedList(); + + /// Returns a concatenation of this with the other + /// [Uint8List] using the `|` operator as a shortcut. + Uint8List operator |(Uint8List other) => plus(other); + + /// Returns a sublist of this from the [start] index to the [end] index. + /// If [end] is greater than the length of the list, it is set to the length. + Uint8List trySublist(int start, [int? end]) { + if (isEmpty) { + return this; + } + + int ending = end ?? length; + if (ending > length) { + ending = length; + } + + return sublist(start, ending); + } + + /// Returns a [List] of [Uint8List] of the specified [chunkSize]. + List chunked(int chunkSize) { + if (isEmpty) { + return []; + } + + return List.generate( + (length / chunkSize).ceil(), + (i) => trySublist(i * chunkSize, (i * chunkSize) + chunkSize), + ); + } + + /// Converts a [Uint8List] to a [String] using the specified [Encoding]. + String toStr({Encoding to = Encoding.utf16}) { + String str; + switch (to) { + case Encoding.utf8: + str = utf8.decode(this); + break; + case Encoding.utf16: + str = String.fromCharCodes(this); + break; + case Encoding.base64: + str = base64.encode(this); + break; + case Encoding.base16: + str = List.generate( + length, + (i) => this[i].toRadixString(16).padLeft(2, '0'), + ).join(); + } + return str; + } +} diff --git a/packages/native_crypto/lib/src/core/utils/chunk_factory.dart b/packages/native_crypto/lib/src/core/utils/chunk_factory.dart new file mode 100644 index 0000000..afbc6b4 --- /dev/null +++ b/packages/native_crypto/lib/src/core/utils/chunk_factory.dart @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/domain/cipher_chunk.dart'; + +/// A factory that creates a [CipherChunk] of type [T] from a [Uint8List]. +typedef ChunkFactory = T Function(Uint8List chunk); diff --git a/packages/native_crypto/lib/src/core/utils/cipher_text.dart b/packages/native_crypto/lib/src/core/utils/cipher_text.dart new file mode 100644 index 0000000..d9e6977 --- /dev/null +++ b/packages/native_crypto/lib/src/core/utils/cipher_text.dart @@ -0,0 +1,47 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/constants/constants.dart'; +import 'package:native_crypto/src/core/extensions/uint8_list_extension.dart'; +import 'package:native_crypto/src/core/utils/chunk_factory.dart'; +import 'package:native_crypto/src/domain/byte_array.dart'; +import 'package:native_crypto/src/domain/cipher_chunk.dart'; + +/// {@template cipher_text} +/// A [CipherText] is a [ByteArray] that is used to store a text encrypted by a +/// Cipher. +/// {@endtemplate} +class CipherText extends ByteArray { + /// {@macro cipher_text} + CipherText( + super.bytes, { + required ChunkFactory this.chunkFactory, + this.chunkSize = Constants.defaultChunkSize, + }) : chunks = bytes.chunked(chunkSize).map(chunkFactory).toList(); + + /// Creates a [CipherText] from a [List] of [CipherChunk]. + CipherText.fromChunks( + this.chunks, { + required ChunkFactory this.chunkFactory, + this.chunkSize = Constants.defaultChunkSize, + }) : super( + chunks.fold( + Uint8List(0), + (acc, chunk) => acc | chunk.bytes, + ), + ); + + /// Factory used to create [CipherChunk] from an Uint8List. + final ChunkFactory? chunkFactory; + + /// List of [CipherChunk] that compose the [CipherText]. + final List chunks; + + /// Size of one chunk. + final int chunkSize; +} diff --git a/packages/native_crypto/lib/src/core/utils/platform.dart b/packages/native_crypto/lib/src/core/utils/platform.dart new file mode 100644 index 0000000..0555237 --- /dev/null +++ b/packages/native_crypto/lib/src/core/utils/platform.dart @@ -0,0 +1,9 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +NativeCryptoPlatform platform = NativeCryptoPlatform.instance; diff --git a/packages/native_crypto/lib/src/core/utils/utils.dart b/packages/native_crypto/lib/src/core/utils/utils.dart new file mode 100644 index 0000000..a673ed2 --- /dev/null +++ b/packages/native_crypto/lib/src/core/utils/utils.dart @@ -0,0 +1,8 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +export 'chunk_factory.dart'; +export 'cipher_text.dart'; diff --git a/packages/native_crypto/lib/src/digest/digest.dart b/packages/native_crypto/lib/src/digest/digest.dart new file mode 100644 index 0000000..d5ab308 --- /dev/null +++ b/packages/native_crypto/lib/src/digest/digest.dart @@ -0,0 +1,8 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +export 'hash.dart'; +export 'hmac.dart'; diff --git a/packages/native_crypto/lib/src/digest/hash.dart b/packages/native_crypto/lib/src/digest/hash.dart new file mode 100644 index 0000000..30f33c7 --- /dev/null +++ b/packages/native_crypto/lib/src/digest/hash.dart @@ -0,0 +1,62 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/enums/hash_algorithm.dart'; +import 'package:native_crypto/src/core/utils/platform.dart'; +import 'package:native_crypto/src/domain/hash.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +class _Hash extends Hash { + const _Hash(this.algorithm); + + @override + Future digest(Uint8List data) async { + final hash = await platform.hash(data, algorithm: algorithm.name); + + // TODO(hpcl): move these checks to the platform interface + if (hash == null) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes', + ); + } + + if (hash.isEmpty) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned no data.', + ); + } + + return hash; + } + + @override + final HashAlgorithm algorithm; +} + +/// A [Hash] that uses the SHA-256 algorithm. +class Sha256 extends _Hash { + factory Sha256() => _instance ??= Sha256._(); + Sha256._() : super(HashAlgorithm.sha256); + static Sha256? _instance; +} + +/// A [Hash] that uses the SHA-384 algorithm. +class Sha384 extends _Hash { + factory Sha384() => _instance ??= Sha384._(); + Sha384._() : super(HashAlgorithm.sha384); + static Sha384? _instance; +} + +/// A [Hash] that uses the SHA-512 algorithm. +class Sha512 extends _Hash { + factory Sha512() => _instance ??= Sha512._(); + Sha512._() : super(HashAlgorithm.sha512); + static Sha512? _instance; +} diff --git a/packages/native_crypto/lib/src/digest/hmac.dart b/packages/native_crypto/lib/src/digest/hmac.dart new file mode 100644 index 0000000..69419b0 --- /dev/null +++ b/packages/native_crypto/lib/src/digest/hmac.dart @@ -0,0 +1,67 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/enums/hash_algorithm.dart'; +import 'package:native_crypto/src/core/utils/platform.dart'; +import 'package:native_crypto/src/domain/hmac.dart'; +import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +class _Hmac extends Hmac { + const _Hmac(this.algorithm); + + @override + Future digest(Uint8List data, SecretKey key) async { + final hmac = await platform.hmac( + data, + key: key.bytes, + algorithm: algorithm.name, + ); + + // TODO(hpcl): move these checks to the platform interface + if (hmac == null) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes', + ); + } + + if (hmac.isEmpty) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned no data.', + ); + } + + return hmac; + } + + @override + final HashAlgorithm algorithm; +} + +/// A [Hmac] that uses the SHA-256 algorithm. +class HmacSha256 extends _Hmac { + factory HmacSha256() => _instance ??= HmacSha256._(); + HmacSha256._() : super(HashAlgorithm.sha256); + static HmacSha256? _instance; +} + +/// A [Hmac] that uses the SHA-384 algorithm. +class HmacSha384 extends _Hmac { + factory HmacSha384() => _instance ??= HmacSha384._(); + HmacSha384._() : super(HashAlgorithm.sha384); + static HmacSha384? _instance; +} + +/// A [Hmac] that uses the SHA-512 algorithm. +class HmacSha512 extends _Hmac { + factory HmacSha512() => _instance ??= HmacSha512._(); + HmacSha512._() : super(HashAlgorithm.sha512); + static HmacSha512? _instance; +} diff --git a/packages/native_crypto/lib/src/domain/base_key.dart b/packages/native_crypto/lib/src/domain/base_key.dart new file mode 100644 index 0000000..2dc92cb --- /dev/null +++ b/packages/native_crypto/lib/src/domain/base_key.dart @@ -0,0 +1,37 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:native_crypto/src/domain/byte_array.dart'; + +/// {@template base_key} +/// Represents a key in NativeCrypto. +/// +/// [BaseKey] is a [ByteArray] that can be used to store keys. +/// +/// This interface is implemented by all the key classes. +/// +/// Note: [BaseKey] is named [BaseKey] instead of Key to avoid conflicts with +/// the Key class from Flutter. +/// {@endtemplate} +abstract class BaseKey extends ByteArray { + /// {@macro base_key} + const BaseKey(super.bytes); + + /// Creates a [BaseKey] from a [List]. + BaseKey.fromList(super.list) : super.fromList(); + + /// Creates a [BaseKey] from a [String] encoded in UTF-8. + BaseKey.fromUtf8(super.encoded) : super.fromUtf8(); + + /// Creates a [BaseKey] from a [String] encoded in UTF-16. + BaseKey.fromUtf16(super.encoded) : super.fromUtf16(); + + /// Creates a [BaseKey] from a [String] encoded in Hexadecimal. + BaseKey.fromBase16(super.encoded) : super.fromBase16(); + + /// Creates a [BaseKey] from a [String] encoded in Base64. + BaseKey.fromBase64(super.encoded) : super.fromBase64(); +} diff --git a/packages/native_crypto/lib/src/interfaces/byte_array.dart b/packages/native_crypto/lib/src/domain/byte_array.dart similarity index 61% rename from packages/native_crypto/lib/src/interfaces/byte_array.dart rename to packages/native_crypto/lib/src/domain/byte_array.dart index 7cdef3a..46bae1c 100644 --- a/packages/native_crypto/lib/src/interfaces/byte_array.dart +++ b/packages/native_crypto/lib/src/domain/byte_array.dart @@ -1,48 +1,49 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: byte_array.dart -// Created Date: 16/12/2021 17:54:16 -// Last Modified: 26/05/2022 17:13:27 -// ----- -// Copyright (c) 2021 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; -import 'package:native_crypto/src/utils/encoding.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; +import 'package:equatable/equatable.dart'; +import 'package:native_crypto/src/core/enums/encoding.dart'; +import 'package:native_crypto/src/core/extensions/list_int_extension.dart'; +import 'package:native_crypto/src/core/extensions/string_extension.dart'; +import 'package:native_crypto/src/core/extensions/uint8_list_extension.dart'; +/// {@template byte_array} /// Represents a byte array. /// /// [ByteArray] wraps a [Uint8List] and provides some useful conversion methods. -@immutable -abstract class ByteArray { - final Uint8List _bytes; - - /// Creates a [ByteArray] from a [Uint8List]. +/// {@endtemplate} +abstract class ByteArray extends Equatable { + /// {@macro byte_array} const ByteArray(this._bytes); - /// Creates a [ByteArray] object from a hexdecimal string. - ByteArray.fromBase16(String encoded) - : _bytes = encoded.toBytes(from: Encoding.base16); + /// Creates a [ByteArray] object from a [List] of [int]. + ByteArray.fromList(List list) : _bytes = list.toTypedList(); - /// Creates a [ByteArray] object from a Base64 string. - ByteArray.fromBase64(String encoded) - : _bytes = encoded.toBytes(from: Encoding.base64); + /// Creates an empty [ByteArray] object from a length. + ByteArray.fromLength(int length, {int fill = 0}) + : _bytes = Uint8List(length)..fillRange(0, length, fill); + + /// Creates a [ByteArray] object from an UTF-16 string. + ByteArray.fromUtf16(String encoded) : _bytes = encoded.toBytes(); /// Creates a [ByteArray] object from an UTF-8 string. ByteArray.fromUtf8(String encoded) : _bytes = encoded.toBytes(from: Encoding.utf8); - /// Creates a [ByteArray] object from an UTF-16 string. - ByteArray.fromUtf16(String encoded) : _bytes = encoded.toBytes(); + /// Creates a [ByteArray] object from a Base64 string. + ByteArray.fromBase64(String encoded) + : _bytes = encoded.toBytes(from: Encoding.base64); - /// Creates an empty [ByteArray] object from a length. - ByteArray.fromLength(int length) : _bytes = Uint8List(length); + /// Creates a [ByteArray] object from a hexdecimal string. + ByteArray.fromBase16(String encoded) + : _bytes = encoded.toBytes(from: Encoding.base16); - /// Creates a [ByteArray] object from a [List] of [int]. - ByteArray.fromList(List list) : _bytes = list.toTypedList(); + final Uint8List _bytes; /// Gets the [ByteArray] bytes. Uint8List get bytes => _bytes; @@ -62,31 +63,6 @@ abstract class ByteArray { /// Gets the [ByteArray] length in bytes. int get length => _bytes.length; - /// Gets the [ByteArray] length in bits. - int get bitLength => _bytes.length * 8; - @override - bool operator ==(Object other) { - if (other is ByteArray) { - for (int i = 0; i < _bytes.length; i++) { - if (_bytes[i] != other._bytes[i]) { - return false; - } - } - - return true; - } - - return false; - } - - @override - int get hashCode { - int hash = 0; - for (int i = 0; i < _bytes.length; i++) { - hash = _bytes[i] + (hash << 6) + (hash << 16) - hash; - } - - return hash; - } + List get props => [_bytes]; } diff --git a/packages/native_crypto/lib/src/domain/cipher.dart b/packages/native_crypto/lib/src/domain/cipher.dart new file mode 100644 index 0000000..6ad3ae1 --- /dev/null +++ b/packages/native_crypto/lib/src/domain/cipher.dart @@ -0,0 +1,32 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/utils/cipher_text.dart'; +import 'package:native_crypto/src/domain/cipher_chunk.dart'; + +/// {@template cipher} +/// Abstract class that defines the behavior of a Cipher. +/// {@endtemplate} +abstract class Cipher { + /// {@macro cipher} + const Cipher(); + /// Encrypts a [Uint8List] and returns a [CipherText]. + Future> encrypt(Uint8List plainText); + + /// Decrypts a [CipherText] and returns a [Uint8List]. + Future decrypt(CipherText cipherText); + + /// Encrypts a File located at [plainTextFile] and saves the result + /// at [cipherTextFile]. + Future encryptFile(File plainTextFile, File cipherTextFile); + + /// Decrypts a File located at [cipherTextFile] and saves the result + /// at [plainTextFile]. + Future decryptFile(File cipherTextFile, File plainTextFile); +} diff --git a/packages/native_crypto/lib/src/domain/cipher_chunk.dart b/packages/native_crypto/lib/src/domain/cipher_chunk.dart new file mode 100644 index 0000000..2af6666 --- /dev/null +++ b/packages/native_crypto/lib/src/domain/cipher_chunk.dart @@ -0,0 +1,31 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:native_crypto/src/domain/byte_array.dart'; + +/// {@template cipher_chunk} +/// A [CipherChunk] is a [ByteArray] that is used to store a chunk of data +/// encrypted by a Cipher. +/// {@endtemplate} +abstract class CipherChunk extends ByteArray { + /// {@macro cipher_chunk} + const CipherChunk(super.bytes); + + /// Creates a [CipherChunk] from a [List]. + CipherChunk.fromList(super.list) : super.fromList(); + + /// Creates a [CipherChunk] from a [String] encoded in UTF-8. + CipherChunk.fromUtf8(super.encoded) : super.fromUtf8(); + + /// Creates a [CipherChunk] from a [String] encoded in UTF-16. + CipherChunk.fromUtf16(super.encoded) : super.fromUtf16(); + + /// Creates a [CipherChunk] from a [String] encoded in Hexadecimal. + CipherChunk.fromBase16(super.encoded) : super.fromBase16(); + + /// Creates a [CipherChunk] from a [String] encoded in Base64. + CipherChunk.fromBase64(super.encoded) : super.fromBase64(); +} diff --git a/packages/native_crypto/lib/src/domain/domain.dart b/packages/native_crypto/lib/src/domain/domain.dart new file mode 100644 index 0000000..58cc8b7 --- /dev/null +++ b/packages/native_crypto/lib/src/domain/domain.dart @@ -0,0 +1,14 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +export 'base_key.dart'; +export 'byte_array.dart'; +export 'cipher.dart'; +export 'cipher_chunk.dart'; +export 'hash.dart'; +export 'hmac.dart'; +export 'key_derivation_function.dart'; +export 'random.dart'; diff --git a/packages/native_crypto/lib/src/domain/hash.dart b/packages/native_crypto/lib/src/domain/hash.dart new file mode 100644 index 0000000..f1e181a --- /dev/null +++ b/packages/native_crypto/lib/src/domain/hash.dart @@ -0,0 +1,24 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/enums/hash_algorithm.dart'; + +/// {@template hash} +/// A [Hash] is a one-way function that takes arbitrary-sized data and +/// outputs a fixed-sized hash value. +/// {@endtemplate} +abstract class Hash { + /// {@macro hash} + const Hash(); + + /// The [HashAlgorithm] used by this [Hash]. + HashAlgorithm get algorithm; + + /// Digests the given [data] and returns the hash. + Future digest(Uint8List data); +} diff --git a/packages/native_crypto/lib/src/domain/hmac.dart b/packages/native_crypto/lib/src/domain/hmac.dart new file mode 100644 index 0000000..45b1923 --- /dev/null +++ b/packages/native_crypto/lib/src/domain/hmac.dart @@ -0,0 +1,25 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/enums/hash_algorithm.dart'; +import 'package:native_crypto/src/keys/secret_key.dart'; + +/// {@template hmac} +/// A HMAC is a cryptographic hash that uses a key to sign a message. +/// The receiver verifies the hash by recomputing it using the same key. +/// {@endtemplate} +abstract class Hmac { + /// {@macro hmac} + const Hmac(); + + /// The [HashAlgorithm] used by this [Hmac]. + HashAlgorithm get algorithm; + + /// Digests the given [data] and returns the hmac. + Future digest(Uint8List data, SecretKey key); +} diff --git a/packages/native_crypto/lib/src/domain/key_derivation_function.dart b/packages/native_crypto/lib/src/domain/key_derivation_function.dart new file mode 100644 index 0000000..d337a6b --- /dev/null +++ b/packages/native_crypto/lib/src/domain/key_derivation_function.dart @@ -0,0 +1,27 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +/// {@template key_derivation_function} +/// A [KeyDerivationFunction] is a function that derives a key from an +/// [Uint8List] key material. +/// {@endtemplate} +abstract class KeyDerivationFunction { + /// {@macro key_derivation_function} + const KeyDerivationFunction(); + + /// Derives a key from a [keyMaterial]. + Future derive( + Uint8List keyMaterial, + ); + + /// Verifies a [keyMaterial] against an [expected] value. + Future verify( + Uint8List keyMaterial, + Uint8List expected, + ); +} diff --git a/packages/native_crypto/lib/src/domain/random.dart b/packages/native_crypto/lib/src/domain/random.dart new file mode 100644 index 0000000..7592e21 --- /dev/null +++ b/packages/native_crypto/lib/src/domain/random.dart @@ -0,0 +1,18 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +/// {@template random} +/// A [Random] is a source of random bytes. +/// {@endtemplate} +abstract class Random { + /// {@macro random} + const Random(); + + /// Generates a random [Uint8List] of [length] bytes. + Future generate(int length); +} diff --git a/packages/native_crypto/lib/src/interfaces/base_key.dart b/packages/native_crypto/lib/src/interfaces/base_key.dart deleted file mode 100644 index e8ac27e..0000000 --- a/packages/native_crypto/lib/src/interfaces/base_key.dart +++ /dev/null @@ -1,22 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: base_key.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 26/05/2022 17:40:38 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/interfaces/byte_array.dart'; - -/// Represents a key in NativeCrypto. -/// -/// [BaseKey] is a [ByteArray] that can be used to store keys. -/// -/// This interface is implemented by all the key classes. -abstract class BaseKey extends ByteArray { - const BaseKey(super.bytes); - BaseKey.fromBase16(super.encoded) : super.fromBase16(); - BaseKey.fromBase64(super.encoded) : super.fromBase64(); - BaseKey.fromUtf8(super.input) : super.fromUtf8(); -} diff --git a/packages/native_crypto/lib/src/interfaces/builder.dart b/packages/native_crypto/lib/src/interfaces/builder.dart deleted file mode 100644 index a1b39aa..0000000 --- a/packages/native_crypto/lib/src/interfaces/builder.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: builder.dart -// Created Date: 28/12/2021 12:02:34 -// Last Modified: 23/05/2022 22:38:44 -// ----- -// Copyright (c) 2021 - -// ignore_for_file: one_member_abstracts - -abstract class Builder { - Future build(); -} diff --git a/packages/native_crypto/lib/src/interfaces/cipher.dart b/packages/native_crypto/lib/src/interfaces/cipher.dart deleted file mode 100644 index 58b0d4a..0000000 --- a/packages/native_crypto/lib/src/interfaces/cipher.dart +++ /dev/null @@ -1,53 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 26/05/2022 21:21:07 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; -import 'package:native_crypto/src/utils/cipher_algorithm.dart'; - -/// Represents a cipher in NativeCrypto. -/// -/// In cryptography, a [Cipher] is an algorithm for performing encryption -/// or decryption - a series of well-defined steps that can -/// be followed as a procedure. -/// -/// This interface is implemented by all the ciphers in NativeCrypto. -abstract class Cipher { - static const int _bytesCountPerChunkDefault = 33554432; - static int _bytesCountPerChunk = _bytesCountPerChunkDefault; - - /// Returns the default number of bytes per chunk. - static int get defaultBytesCountPerChunk => _bytesCountPerChunkDefault; - - /// Returns the size of a chunk of data - /// that can be processed by the [Cipher]. - static int get bytesCountPerChunk => Cipher._bytesCountPerChunk; - - /// Sets the size of a chunk of data - /// that can be processed by the [Cipher]. - static set bytesCountPerChunk(int bytesCount) { - _bytesCountPerChunk = bytesCount; - } - - /// Returns the standard algorithm for this [Cipher]. - CipherAlgorithm get algorithm; - - /// Encrypts the [data]. - /// - /// Takes [Uint8List] data as parameter. - /// Returns a [CipherTextWrapper]. - Future encrypt(Uint8List data); - - /// Decrypts the [cipherText] - /// - /// Takes [CipherTextWrapper] as parameter. - /// And returns plain text data as [Uint8List]. - Future decrypt(CipherTextWrapper cipherText); -} diff --git a/packages/native_crypto/lib/src/interfaces/interfaces.dart b/packages/native_crypto/lib/src/interfaces/interfaces.dart deleted file mode 100644 index ef47be3..0000000 --- a/packages/native_crypto/lib/src/interfaces/interfaces.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: interfaces.dart -// Created Date: 23/05/2022 23:03:47 -// Last Modified: 26/05/2022 17:41:06 -// ----- -// Copyright (c) 2022 - -export 'base_key.dart'; -export 'builder.dart'; -export 'byte_array.dart'; -export 'cipher.dart'; -export 'keyderivation.dart'; diff --git a/packages/native_crypto/lib/src/interfaces/keyderivation.dart b/packages/native_crypto/lib/src/interfaces/keyderivation.dart deleted file mode 100644 index ccdbb4b..0000000 --- a/packages/native_crypto/lib/src/interfaces/keyderivation.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: kdf.dart -// Created Date: 18/12/2021 11:56:43 -// Last Modified: 26/05/2022 18:47:15 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/keys/secret_key.dart'; -import 'package:native_crypto/src/utils/kdf_algorithm.dart'; - -/// Represents a Key Derivation Function (KDF) in NativeCrypto. -/// -/// [KeyDerivation] function is a function that takes some -/// parameters and returns a [SecretKey]. -abstract class KeyDerivation { - /// Returns the standard algorithm for this key derivation function - KdfAlgorithm get algorithm; - - /// Derive a [SecretKey]. - Future derive(); -} diff --git a/packages/native_crypto/lib/src/kdf/kdf.dart b/packages/native_crypto/lib/src/kdf/kdf.dart deleted file mode 100644 index cb7d609..0000000 --- a/packages/native_crypto/lib/src/kdf/kdf.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: kdf.dart -// Created Date: 23/05/2022 22:57:11 -// Last Modified: 23/05/2022 23:04:15 -// ----- -// Copyright (c) 2022 - -export 'pbkdf2.dart'; diff --git a/packages/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart index 8ccdadd..21373af 100644 --- a/packages/native_crypto/lib/src/kdf/pbkdf2.dart +++ b/packages/native_crypto/lib/src/kdf/pbkdf2.dart @@ -1,115 +1,93 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: pbkdf2.dart -// Created Date: 17/12/2021 14:50:42 -// Last Modified: 26/05/2022 23:19:46 -// ----- -// Copyright (c) 2021 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. -import 'dart:typed_data'; - -import 'package:native_crypto/src/interfaces/keyderivation.dart'; -import 'package:native_crypto/src/keys/secret_key.dart'; -import 'package:native_crypto/src/platform.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; -import 'package:native_crypto/src/utils/hash_algorithm.dart'; -import 'package:native_crypto/src/utils/kdf_algorithm.dart'; +import 'package:flutter/foundation.dart'; +import 'package:native_crypto/native_crypto.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -/// Represent a PBKDF2 Key Derivation Function (KDF) in NativeCrypto. -/// -/// [Pbkdf2] is a function that takes password, salt, iteration count and -/// derive a [SecretKey] of specified length. -class Pbkdf2 extends KeyDerivation { - final int _keyBytesCount; - final int _iterations; - final HashAlgorithm _hash; +/// {@template pbkdf2} +/// A PBKDF2 is a password-based key derivation function. +/// {@endtemplate} +class Pbkdf2 extends KeyDerivationFunction { + /// {@macro pbkdf2} + const Pbkdf2({ + required this.hashAlgorithm, + required this.iterations, + required this.salt, + required this.length, + }); + + /// The [HashAlgorithm] used by this [Pbkdf2]. + final HashAlgorithm hashAlgorithm; + + /// The number of iterations. + final int iterations; + + /// The salt. + final Uint8List salt; + + /// The length of the derived key in bytes. + final int length; @override - KdfAlgorithm get algorithm => KdfAlgorithm.pbkdf2; + Future derive(Uint8List keyMaterial) async { + if (length == 0) { + // If the length is 0, return an empty list + return Uint8List(0); + } - Pbkdf2({ - required int keyBytesCount, - required int iterations, - HashAlgorithm algorithm = HashAlgorithm.sha256, - }) : _keyBytesCount = keyBytesCount, - _iterations = iterations, - _hash = algorithm { - if (keyBytesCount < 0) { - throw NativeCryptoException( - message: 'keyBytesCount must be positive.', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); + if (length < 0) { + throw ArgumentError.value(length, 'length', 'must be positive'); } if (iterations <= 0) { - throw NativeCryptoException( - message: 'iterations must be strictly positive.', - code: NativeCryptoExceptionCode.invalid_argument.code, + throw ArgumentError.value( + iterations, + 'iterations', + 'must greater than 0', ); } + + // Call the platform interface to derive the key + final bytes = await platform.pbkdf2( + password: keyMaterial, + salt: salt, + iterations: iterations, + length: length, + hashAlgorithm: hashAlgorithm.name, + ); + + // TODO(hpcl): move these checks to the platform interface + if (bytes == null) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes', + ); + } + + if (bytes.length != length) { + throw NativeCryptoException( + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned bytes of wrong length: ' + 'expected $length, got ${bytes.length}', + ); + } + + return bytes; } @override - Future derive({String? password, String? salt}) async { - Uint8List? derivation; + Future verify(Uint8List keyMaterial, Uint8List expected) => + derive(keyMaterial).then((actual) { + if (actual.length != expected.length) { + return false; + } + return listEquals(actual, expected); + }); - if (_keyBytesCount == 0) { - return SecretKey(Uint8List(0)); - } - if (password.isNull) { - throw NativeCryptoException( - message: 'Password cannot be null.', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); - } - - if (salt.isNull) { - throw NativeCryptoException( - message: 'Salt cannot be null.', - code: NativeCryptoExceptionCode.invalid_argument.code, - ); - } - - try { - derivation = await platform.pbkdf2( - password!, - salt!, - _keyBytesCount, - _iterations, - _hash.name, - ); - } catch (e, s) { - throw NativeCryptoException( - message: '$e', - code: NativeCryptoExceptionCode.platform_throws.code, - stackTrace: s, - ); - } - - if (derivation.isNull) { - throw NativeCryptoException( - message: 'Failed to derive a key! Platform returned null.', - code: NativeCryptoExceptionCode.platform_returned_null.code, - ); - } - - if (derivation!.isEmpty) { - throw NativeCryptoException( - message: 'Failed to derive a key! Platform returned no data.', - code: NativeCryptoExceptionCode.platform_returned_empty_data.code, - ); - } - - if (derivation.length != _keyBytesCount) { - throw NativeCryptoException( - message: 'Failed to derive a key! Platform returned ' - '${derivation.length} bytes, but expected $_keyBytesCount bytes.', - code: NativeCryptoExceptionCode.platform_returned_invalid_data.code, - ); - } - - return SecretKey(derivation); - } + Future call({required String password}) => + derive(password.toBytes()).then(SecretKey.new); } diff --git a/packages/native_crypto/lib/src/keys/keys.dart b/packages/native_crypto/lib/src/keys/keys.dart deleted file mode 100644 index 912bb39..0000000 --- a/packages/native_crypto/lib/src/keys/keys.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: keys.dart -// Created Date: 23/05/2022 23:04:04 -// Last Modified: 23/05/2022 23:04:07 -// ----- -// Copyright (c) 2022 - -export 'secret_key.dart'; diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index e30b87b..3c6b0c5 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -1,60 +1,43 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: secret_key.dart -// Created Date: 28/12/2021 13:36:54 -// Last Modified: 26/05/2022 23:13:10 -// ----- -// Copyright (c) 2021 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. -import 'dart:typed_data'; - -import 'package:native_crypto/src/interfaces/base_key.dart'; -import 'package:native_crypto/src/interfaces/cipher.dart'; -import 'package:native_crypto/src/platform.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; +import 'package:native_crypto/src/domain/base_key.dart'; +import 'package:native_crypto/src/random/secure_random.dart'; +/// {@template secret_key} /// Represents a secret key in NativeCrypto. /// /// [SecretKey] is a [BaseKey] that can be used to store secret keys. /// A [SecretKey] is a key that can be used to encrypt or decrypt data with -/// a symmetric [Cipher]. +/// a symmetric Cipher. +/// {@endtemplate} class SecretKey extends BaseKey { + /// {@macro secret_key} const SecretKey(super.bytes); + + /// Creates a [SecretKey] from a [List]. + SecretKey.fromList(super.list) : super.fromList(); + + /// Creates a [SecretKey] from a [String] encoded in UTF-8. + SecretKey.fromUtf8(super.encoded) : super.fromUtf8(); + + /// Creates a [SecretKey] from a [String] encoded in UTF-16. + SecretKey.fromUtf16(super.encoded) : super.fromUtf16(); + + /// Creates a [SecretKey] from a [String] encoded in Hexadecimal. SecretKey.fromBase16(super.encoded) : super.fromBase16(); + + /// Creates a [SecretKey] from a [String] encoded in Base64. SecretKey.fromBase64(super.encoded) : super.fromBase64(); - SecretKey.fromUtf8(super.input) : super.fromUtf8(); - static Future fromSecureRandom(int bitsCount) async { - Uint8List? key; - if (bitsCount == 0) { - return SecretKey(Uint8List(0)); - } + /// Generates a random [SecretKey] of the given [length] in bytes. + static Future fromSecureRandom(int length) async { + const random = SecureRandom(); + final bytes = await random.generate(length); - try { - key = await platform.generateSecretKey(bitsCount); - } catch (e, s) { - throw NativeCryptoException( - message: '$e', - code: NativeCryptoExceptionCode.platform_throws.code, - stackTrace: s, - ); - } - if (key.isNull) { - throw NativeCryptoException( - message: 'Failed to generate a secret key! Platform returned null.', - code: NativeCryptoExceptionCode.platform_returned_null.code, - ); - } - - if (key!.isEmpty) { - throw NativeCryptoException( - message: 'Failed to generate a secret key! ' - 'Platform returned no data.', - code: NativeCryptoExceptionCode.platform_returned_empty_data.code, - ); - } - return SecretKey(key); + return SecretKey(bytes); } } diff --git a/packages/native_crypto/lib/src/platform.dart b/packages/native_crypto/lib/src/platform.dart deleted file mode 100644 index 5d62b5e..0000000 --- a/packages/native_crypto/lib/src/platform.dart +++ /dev/null @@ -1,12 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: platform.dart -// Created Date: 27/12/2021 22:03:58 -// Last Modified: 25/05/2022 10:09:18 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -NativeCryptoPlatform platform = NativeCryptoPlatform.instance; diff --git a/packages/native_crypto/lib/src/random/secure_random.dart b/packages/native_crypto/lib/src/random/secure_random.dart new file mode 100644 index 0000000..c769e29 --- /dev/null +++ b/packages/native_crypto/lib/src/random/secure_random.dart @@ -0,0 +1,50 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/utils/platform.dart'; +import 'package:native_crypto/src/domain/random.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +/// {@template secure_random} +/// A [SecureRandom] is a source of secure random bytes. +/// {@endtemplate} +class SecureRandom extends Random { + /// {@macro secure_random} + const SecureRandom(); + + @override + Future generate(int length) async { + if (length < 0) { + throw ArgumentError.value(length, 'length', 'must be positive'); + } + if (length == 0) { + // If the length is 0, return an empty list + return Uint8List(0); + } + // Call the platform interface to generate the secure random bytes + final bytes = await platform.generateSecureRandom(length); + + // TODO(hpcl): move these checks to the platform interface + if (bytes == null) { + throw const NativeCryptoException( + code: NativeCryptoExceptionCode.nullError, + message: 'Platform returned null bytes', + ); + } + + if (bytes.length != length) { + throw NativeCryptoException( + code: NativeCryptoExceptionCode.invalidData, + message: 'Platform returned bytes of wrong length: ' + 'expected $length, got ${bytes.length}', + ); + } + + return bytes; + } +} diff --git a/packages/native_crypto/lib/src/utils/cipher_algorithm.dart b/packages/native_crypto/lib/src/utils/cipher_algorithm.dart deleted file mode 100644 index 2ba968c..0000000 --- a/packages/native_crypto/lib/src/utils/cipher_algorithm.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher_algorithm.dart -// Created Date: 23/05/2022 22:07:54 -// Last Modified: 26/05/2022 18:52:32 -// ----- -// Copyright (c) 2022 - -/// Represents different cipher algorithms -enum CipherAlgorithm { aes } diff --git a/packages/native_crypto/lib/src/utils/encoding.dart b/packages/native_crypto/lib/src/utils/encoding.dart deleted file mode 100644 index b7ddd80..0000000 --- a/packages/native_crypto/lib/src/utils/encoding.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: encoding.dart -// Created Date: 26/05/2022 12:12:34 -// Last Modified: 26/05/2022 12:18:09 -// ----- -// Copyright (c) 2022 - -enum Encoding { utf8, utf16, base64, base16 } diff --git a/packages/native_crypto/lib/src/utils/extensions.dart b/packages/native_crypto/lib/src/utils/extensions.dart deleted file mode 100644 index fc2a799..0000000 --- a/packages/native_crypto/lib/src/utils/extensions.dart +++ /dev/null @@ -1,101 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: extensions.dart -// Created Date: 26/05/2022 12:12:48 -// Last Modified: 27/05/2022 12:26:55 -// ----- -// Copyright (c) 2022 - -import 'dart:convert'; -import 'dart:developer' as developer; -import 'dart:typed_data'; - -import 'package:native_crypto/src/utils/encoding.dart'; - -extension ObjectX on Object? { - /// Returns `true` if the object is `null`. - bool get isNull => this == null; - - /// Returns `true` if the object is **not** `null`. - bool get isNotNull => this != null; - - /// Prints the object to the console. - void log() => developer.log(toString()); -} - -extension ListIntX on List { - /// Converts a [List] of int to a [Uint8List]. - Uint8List toTypedList() => Uint8List.fromList(this); -} - -extension ListUint8ListX on List { - /// Reduce a [List] of [Uint8List] to a [Uint8List]. - Uint8List combine() { - if (isEmpty) return Uint8List(0); - return reduce((value, element) => value.plus(element)); - } -} - -extension StringX on String { - /// Converts a [String] to a [Uint8List] using the specified [Encoding]. - Uint8List toBytes({final Encoding from = Encoding.utf16}) { - Uint8List bytes; - switch (from) { - case Encoding.utf8: - bytes = utf8.encode(this).toTypedList(); - break; - case Encoding.utf16: - bytes = runes.toList().toTypedList(); - break; - case Encoding.base64: - bytes = base64.decode(this); - break; - case Encoding.base16: - assert(length.isEven, 'String needs to be an even length.'); - bytes = List.generate( - length ~/ 2, - (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), - ).toList().toTypedList(); - } - return bytes; - } -} - -extension Uint8ListX on Uint8List { - /// Converts a [Uint8List] to a [String] using the specified [Encoding]. - String toStr({final Encoding to = Encoding.utf16}) { - String str; - switch (to) { - case Encoding.utf8: - str = utf8.decode(this); - break; - case Encoding.utf16: - str = String.fromCharCodes(this); - break; - case Encoding.base64: - str = base64.encode(this); - break; - case Encoding.base16: - str = List.generate( - length, - (i) => this[i].toRadixString(16).padLeft(2, '0'), - ).join(); - } - return str; - } - - /// Returns a concatenation of this with the other [Uint8List]. - Uint8List plus(final Uint8List other) => [...this, ...other].toTypedList(); - - /// Returns a sublist of this from the [start] index to the [end] index. - /// If [end] is greater than the length of the list, it is set to the length - Uint8List trySublist(int start, [int? end]) { - if (isEmpty) return this; - - int ending = end ?? length; - if (ending > length) ending = length; - - return sublist(start, ending); - } -} diff --git a/packages/native_crypto/lib/src/utils/hash_algorithm.dart b/packages/native_crypto/lib/src/utils/hash_algorithm.dart deleted file mode 100644 index 4538ef5..0000000 --- a/packages/native_crypto/lib/src/utils/hash_algorithm.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: hash_algorithm.dart -// Created Date: 23/05/2022 22:01:59 -// Last Modified: 26/05/2022 22:59:04 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:native_crypto/src/platform.dart'; -import 'package:native_crypto/src/utils/extensions.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -/// Defines the hash algorithms. -enum HashAlgorithm { - sha256, - sha384, - sha512; - - /// Digest the [data] using this [HashAlgorithm]. - Future digest(Uint8List data) async { - Uint8List? hash; - try { - hash = await platform.digest(data, name); - } catch (e, s) { - throw NativeCryptoException( - message: '$e', - code: NativeCryptoExceptionCode.platform_throws.code, - stackTrace: s, - ); - } - - if (hash.isNull) { - throw NativeCryptoException( - message: 'Failed to digest data! Platform returned null.', - code: NativeCryptoExceptionCode.platform_returned_null.code, - ); - } - - if (hash!.isEmpty) { - throw NativeCryptoException( - message: 'Failed to digest data! Platform returned no data.', - code: NativeCryptoExceptionCode.platform_returned_empty_data.code, - ); - } - - return hash; - } -} diff --git a/packages/native_crypto/lib/src/utils/kdf_algorithm.dart b/packages/native_crypto/lib/src/utils/kdf_algorithm.dart deleted file mode 100644 index 68d6a76..0000000 --- a/packages/native_crypto/lib/src/utils/kdf_algorithm.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: kdf_algorithm.dart -// Created Date: 23/05/2022 22:36:24 -// Last Modified: 26/05/2022 18:53:50 -// ----- -// Copyright (c) 2022 - -/// Represents different key derivation functions -enum KdfAlgorithm { pbkdf2 } diff --git a/packages/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml index 72b0d59..e78c351 100644 --- a/packages/native_crypto/pubspec.yaml +++ b/packages/native_crypto/pubspec.yaml @@ -2,22 +2,21 @@ name: native_crypto description: Fast and secure cryptography for Flutter. version: 0.1.1 -publish_to: 'none' +publish_to: "none" environment: sdk: ">=2.17.0 <3.0.0" flutter: ">=2.5.0" dependencies: - flutter: - sdk: flutter - + flutter: { sdk: flutter } + native_crypto_android: git: url: https://github.com/hugo-pcl/native-crypto-flutter.git ref: native_crypto_android-v0.1.1 path: packages/native_crypto_android - + native_crypto_ios: git: url: https://github.com/hugo-pcl/native-crypto-flutter.git @@ -29,19 +28,19 @@ dependencies: url: https://github.com/hugo-pcl/native-crypto-flutter.git ref: native_crypto_platform_interface-v0.1.1 path: packages/native_crypto_platform_interface + equatable: ^2.0.5 dev_dependencies: - flutter_test: - sdk: flutter + flutter_test: { sdk: flutter } - mockito: ^5.2.0 - plugin_platform_interface: ^2.1.2 + mockito: ^5.3.2 + plugin_platform_interface: ^2.1.3 wyatt_analysis: - git: - url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages - ref: wyatt_analysis-v2.1.0 - path: packages/wyatt_analysis + hosted: + url: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ + name: wyatt_analysis + version: 2.4.0 flutter: plugin: @@ -49,4 +48,4 @@ flutter: android: default_package: native_crypto_android ios: - default_package: native_crypto_ios \ No newline at end of file + default_package: native_crypto_ios diff --git a/packages/native_crypto/pubspec_overrides.yaml b/packages/native_crypto/pubspec_overrides.yaml new file mode 100644 index 0000000..53d57bc --- /dev/null +++ b/packages/native_crypto/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: native_crypto_android,native_crypto_ios,native_crypto_platform_interface,native_crypto_web +dependency_overrides: + native_crypto_android: + path: ../native_crypto_android + native_crypto_ios: + path: ../native_crypto_ios + native_crypto_platform_interface: + path: ../native_crypto_platform_interface diff --git a/packages/native_crypto/test/mocks/mock_native_crypto_api.dart b/packages/native_crypto/test/mocks/mock_native_crypto_api.dart new file mode 100644 index 0000000..e8b4afa --- /dev/null +++ b/packages/native_crypto/test/mocks/mock_native_crypto_api.dart @@ -0,0 +1,193 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +class MockNativeCryptoAPI implements NativeCryptoAPI { + static Uint8List? Function(int length)? generateSecureRandomFn; + + static Uint8List? Function( + Uint8List data, + String algorithm, + )? hashFn; + + static Uint8List? Function( + Uint8List data, + Uint8List key, + String algorithm, + )? hmacFn; + + static Uint8List? Function( + Uint8List cipherText, + Uint8List key, + String algorithm, + )? decryptFn; + + static bool? Function( + String cipherTextPath, + String plainTextPath, + Uint8List key, + String algorithm, + )? decryptFileFn; + + static Uint8List? Function( + Uint8List plainText, + Uint8List key, + String algorithm, + )? encryptFn; + + static bool? Function( + String plainTextPath, + String cipherTextPath, + Uint8List key, + String algorithm, + )? encryptFileFn; + + static Uint8List? Function( + Uint8List plainText, + Uint8List key, + Uint8List iv, + String algorithm, + )? encryptWithIVFn; + + static Uint8List? Function( + Uint8List password, + Uint8List salt, + int iterations, + int length, + String algorithm, + )? pbkdf2Fn; + + @override + Future decrypt(DecryptRequest argRequest) async => + decryptFn != null + ? DecryptResponse( + plainText: decryptFn!( + argRequest.cipherText!, + argRequest.key!, + argRequest.algorithm!, + ), + ) + : DecryptResponse( + plainText: Uint8List.fromList([1, 2, 3]), + ); + + @override + Future decryptFile( + DecryptFileRequest argRequest, + ) async => + decryptFileFn != null + ? DecryptFileResponse( + success: decryptFileFn!( + argRequest.cipherTextPath!, + argRequest.plainTextPath!, + argRequest.key!, + argRequest.algorithm!, + ), + ) + : DecryptFileResponse(success: true); + + @override + Future encrypt(EncryptRequest argRequest) async => + encryptFn != null + ? EncryptResponse( + cipherText: encryptFn!( + argRequest.plainText!, + argRequest.key!, + argRequest.algorithm!, + ), + ) + : EncryptResponse( + cipherText: Uint8List.fromList([1, 2, 3]), + ); + + @override + Future encryptFile( + EncryptFileRequest argRequest, + ) async => + encryptFileFn != null + ? EncryptFileResponse( + success: encryptFileFn!( + argRequest.plainTextPath!, + argRequest.cipherTextPath!, + argRequest.key!, + argRequest.algorithm!, + ), + ) + : EncryptFileResponse(success: true); + + @override + Future encryptWithIV( + EncryptWithIVRequest argRequest, + ) async => + encryptWithIVFn != null + ? EncryptResponse( + cipherText: encryptWithIVFn!( + argRequest.plainText!, + argRequest.key!, + argRequest.iv!, + argRequest.algorithm!, + ), + ) + : EncryptResponse( + cipherText: Uint8List.fromList([1, 2, 3]), + ); + + @override + Future generateSecureRandom( + GenerateSecureRandomRequest argRequest, + ) async => + generateSecureRandomFn != null + ? GenerateSecureRandomResponse( + random: generateSecureRandomFn!(argRequest.length!), + ) + : GenerateSecureRandomResponse( + random: Uint8List.fromList([1, 2, 3]), + ); + + @override + Future hash(HashRequest argRequest) async => hashFn != null + ? HashResponse( + hash: hashFn!( + argRequest.data!, + argRequest.algorithm!, + ), + ) + : HashResponse( + hash: Uint8List.fromList([1, 2, 3]), + ); + + @override + Future hmac(HmacRequest argRequest) async => hmacFn != null + ? HmacResponse( + hmac: hmacFn!( + argRequest.data!, + argRequest.key!, + argRequest.algorithm!, + ), + ) + : HmacResponse( + hmac: Uint8List.fromList([1, 2, 3]), + ); + + @override + Future pbkdf2(Pbkdf2Request argRequest) async => + pbkdf2Fn != null + ? Pbkdf2Response( + key: pbkdf2Fn!( + argRequest.password!, + argRequest.salt!, + argRequest.iterations!, + argRequest.length!, + argRequest.hashAlgorithm!, + ), + ) + : Pbkdf2Response( + key: Uint8List.fromList([1, 2, 3]), + ); +} diff --git a/packages/native_crypto/test/mocks/mock_native_crypto_platform.dart b/packages/native_crypto/test/mocks/mock_native_crypto_platform.dart deleted file mode 100644 index 6e69cb9..0000000 --- a/packages/native_crypto/test/mocks/mock_native_crypto_platform.dart +++ /dev/null @@ -1,192 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: mock_native_crypto_platform.dart -// Created Date: 25/05/2022 23:34:34 -// Last Modified: 26/05/2022 11:40:24 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockNativeCryptoPlatform extends Fake - with MockPlatformInterfaceMixin - implements NativeCryptoPlatform { - Uint8List? data; - List? dataAsList; - Uint8List? key; - String? algorithm; - int? bitsCount; - String? password; - String? salt; - int? keyBytesCount; - int? iterations; - - Uint8List? Function()? response; - List? Function()? responseAsList; - - // ignore: use_setters_to_change_properties - void setResponse(Uint8List? Function()? response) { - this.response = response; - } - - // ignore: use_setters_to_change_properties - void setResponseAsList(List? Function()? responseAsList) { - this.responseAsList = responseAsList; - } - - void setDecryptExpectations({ - required Uint8List data, - required Uint8List key, - required String algorithm, - }) { - this.data = data; - this.key = key; - this.algorithm = algorithm; - } - - @override - Future decrypt( - Uint8List data, - Uint8List key, - String algorithm, - ) async { - expect(data, this.data); - expect(key, this.key); - expect(algorithm, this.algorithm); - return response?.call(); - } - - void setDecryptAsListExpectations({ - required List data, - required Uint8List key, - required String algorithm, - }) { - dataAsList = data; - this.key = key; - this.algorithm = algorithm; - } - - @override - Future decryptAsList( - List data, - Uint8List key, - String algorithm, - ) async { - expect(data, dataAsList); - expect(key, this.key); - expect(algorithm, this.algorithm); - - return response?.call(); - } - - void setDigestExpectations({ - required Uint8List data, - required String algorithm, - }) { - this.data = data; - this.algorithm = algorithm; - } - - @override - Future digest(Uint8List data, String algorithm) async { - expect(data, this.data); - expect(algorithm, this.algorithm); - - return response?.call(); - } - - void setEncryptExpectations({ - required Uint8List data, - required Uint8List key, - required String algorithm, - }) { - this.data = data; - this.key = key; - this.algorithm = algorithm; - } - - @override - Future encrypt( - Uint8List data, - Uint8List key, - String algorithm, - ) async { - expect(data, this.data); - expect(key, this.key); - expect(algorithm, this.algorithm); - - return response?.call(); - } - - void setEncryptAsListExpectations({ - required Uint8List data, - required Uint8List key, - required String algorithm, - }) => - setEncryptExpectations( - data: data, - key: key, - algorithm: algorithm, - ); - - @override - Future?> encryptAsList( - Uint8List data, - Uint8List key, - String algorithm, - ) async { - expect(data, this.data); - expect(key, this.key); - expect(algorithm, this.algorithm); - - return responseAsList?.call(); - } - - // ignore: use_setters_to_change_properties - void setGenerateKeyExpectations({required int bitsCount}) { - this.bitsCount = bitsCount; - } - - @override - Future generateSecretKey(int bitsCount) async { - expect(bitsCount, this.bitsCount); - - return response?.call(); - } - - void setPbkdf2Expectations({ - required String password, - required String salt, - required int keyBytesCount, - required int iterations, - required String algorithm, - }) { - this.password = password; - this.salt = salt; - this.iterations = iterations; - this.keyBytesCount = keyBytesCount; - this.algorithm = algorithm; - } - - @override - Future pbkdf2( - String password, - String salt, - int keyBytesCount, - int iterations, - String algorithm, - ) async { - expect(password, this.password); - expect(salt, this.salt); - expect(keyBytesCount, this.keyBytesCount); - expect(iterations, this.iterations); - expect(algorithm, this.algorithm); - - return response?.call(); - } -} diff --git a/packages/native_crypto/test/src/aes_cipher_test.dart b/packages/native_crypto/test/src/aes_cipher_test.dart deleted file mode 100644 index f1d0f39..0000000 --- a/packages/native_crypto/test/src/aes_cipher_test.dart +++ /dev/null @@ -1,323 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: aes_cipher_test.dart -// Created Date: 26/05/2022 23:20:53 -// Last Modified: 27/05/2022 16:39:44 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto/native_crypto.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -import '../mocks/mock_native_crypto_platform.dart'; - -void main() { - final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); - NativeCryptoPlatform.instance = mock; - - setUp(() { - Cipher.bytesCountPerChunk = Cipher.defaultBytesCountPerChunk; - }); - - group('Constructor', () { - test('throws on invalid key length', () { - expect( - () => AES(SecretKey(Uint8List(0))), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_key_length', - ) - .having( - (e) => e.message, - 'message', - contains('Invalid key'), - ), - ), - ); - }); - - test('creates a valid instance', () { - expect( - AES( - SecretKey(Uint8List(16)), - ), - isA(), - ); - }); - }); - - group('encrypt', () { - test('returns a valid cipher text wrapper', () async { - mock - ..setEncryptExpectations( - data: Uint8List(16), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => Uint8List(16 + 28)); - - final aes = AES(SecretKey(Uint8List(16))); - - expect( - await aes.encrypt(Uint8List(16)), - isA().having((e) => e.isSingle, 'is single', isTrue), - ); - }); - - test('returns a valid cipher text with multiple chunks', () async { - mock - ..setEncryptExpectations( - data: Uint8List(16), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => Uint8List(16 + 28)); // Returns 1 encrypted chunk - Cipher.bytesCountPerChunk = 16; - final aes = AES(SecretKey(Uint8List(16))); - - expect( - await aes.encrypt(Uint8List(16 * 3)), - isA().having((e) => e.isList, 'is list', isTrue), - ); - }); - - test('handles returning empty list', () async { - mock - ..setEncryptExpectations( - data: Uint8List(16), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => Uint8List(0)); - - final aes = AES(SecretKey(Uint8List(16))); - - await expectLater( - () => aes.encrypt(Uint8List(16)), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_empty_data', - ), - ), - ); - }); - - test('handles returning null', () async { - mock - ..setEncryptExpectations( - data: Uint8List(16), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => null); - - final aes = AES(SecretKey(Uint8List(16))); - - await expectLater( - () => aes.encrypt(Uint8List(16)), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_null', - ), - ), - ); - }); - - test('handles throwing PlatformException', () async { - mock - ..setEncryptExpectations( - data: Uint8List(16), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse( - () => throw PlatformException( - code: 'native_crypto', - message: 'dummy error', - ), - ); - - final aes = AES(SecretKey(Uint8List(16))); - - await expectLater( - () => aes.encrypt(Uint8List(16)), - throwsA( - isA() - .having( - (e) => e.message, - 'message', - contains( - 'PlatformException(native_crypto, dummy error, null, null)', - ), - ) - .having( - (e) => e.code, - 'code', - 'platform_throws', - ), - ), - ); - }); - }); - - group('decrypt', () { - test('returns a valid Uint8List', () async { - mock - ..setDecryptExpectations( - data: Uint8List(16 + 28), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => Uint8List(16)); - - final aes = AES(SecretKey(Uint8List(16))); - final bytes = Uint8List(16 + 28); - final wrapper = CipherTextWrapper.fromBytes( - bytes, - ivLength: 12, - tagLength: 16, - ); - - expect( - await aes.decrypt(wrapper), - isA().having((e) => e.length, 'length', 16), - ); - }); - - test('returns a valid Uint8List on decrypting multiple chunks', () async { - const int chunkSize = 8; - mock - ..setDecryptExpectations( - data: Uint8List(chunkSize + 28), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => Uint8List(chunkSize)); - Cipher.bytesCountPerChunk = chunkSize; - final aes = AES(SecretKey(Uint8List(16))); - final bytes = Uint8List((chunkSize + 28) * 3); - final wrapper = CipherTextWrapper.fromBytes( - bytes, - ivLength: 12, - tagLength: 16, - ); - - expect( - await aes.decrypt(wrapper), - isA().having((e) => e.length, 'length', chunkSize * 3), - ); - }); - - test('handles returning empty list', () async { - mock - ..setDecryptExpectations( - data: Uint8List(16 + 28), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => Uint8List(0)); - - final aes = AES(SecretKey(Uint8List(16))); - final bytes = Uint8List(16 + 28); - final wrapper = CipherTextWrapper.fromBytes( - bytes, - ivLength: 12, - tagLength: 16, - ); - - await expectLater( - () => aes.decrypt(wrapper), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_empty_data', - ), - ), - ); - }); - - test('handles returning null', () async { - mock - ..setDecryptExpectations( - data: Uint8List(16 + 28), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse(() => null); - - final aes = AES(SecretKey(Uint8List(16))); - final bytes = Uint8List(16 + 28); - final wrapper = CipherTextWrapper.fromBytes( - bytes, - ivLength: 12, - tagLength: 16, - ); - - await expectLater( - () => aes.decrypt(wrapper), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_null', - ), - ), - ); - }); - - test('handles throwing PlatformException', () async { - mock - ..setDecryptExpectations( - data: Uint8List(16 + 28), - key: Uint8List(16), - algorithm: 'aes', - ) - ..setResponse( - () => throw PlatformException( - code: 'native_crypto', - message: 'dummy error', - ), - ); - - final aes = AES(SecretKey(Uint8List(16))); - final bytes = Uint8List(16 + 28); - final wrapper = CipherTextWrapper.fromBytes( - bytes, - ivLength: 12, - tagLength: 16, - ); - - await expectLater( - () => aes.decrypt(wrapper), - throwsA( - isA() - .having( - (e) => e.message, - 'message', - contains( - 'PlatformException(native_crypto, dummy error, null, null)', - ), - ) - .having( - (e) => e.code, - 'code', - 'platform_throws', - ), - ), - ); - }); - }); -} diff --git a/packages/native_crypto/test/src/cipher_text_test.dart b/packages/native_crypto/test/src/cipher_text_test.dart deleted file mode 100644 index 16d98a5..0000000 --- a/packages/native_crypto/test/src/cipher_text_test.dart +++ /dev/null @@ -1,192 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher_text_test.dart -// Created Date: 26/05/2022 20:45:38 -// Last Modified: 26/05/2022 21:29:51 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto/native_crypto.dart'; - -void main() { - setUp(() { - Cipher.bytesCountPerChunk = Cipher.defaultBytesCountPerChunk; - }); - - group('fromBytes', () { - test('throws if length is not the one expected', () { - final Uint8List bytes = Uint8List.fromList([1, 2, 3, 4, 5]); - expect( - () => CipherText.fromBytes( - bytes, - ivLength: 1, - messageLength: 1, - tagLength: 1, - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('Invalid cipher text length'), - ), - ), - ); - }); - - test('throws if length is bigger than expected', () { - final Uint8List bytes = Uint8List.fromList([1, 3, 3, 3, 1]); - Cipher.bytesCountPerChunk = 2; - expect( - () => CipherText.fromBytes( - bytes, - ivLength: 1, - messageLength: 3, - tagLength: 1, - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('Cipher text is too big'), - ), - ), - ); - }); - - test('throws if data is empty', () { - final Uint8List bytes = Uint8List(0); - expect( - () => CipherText.fromBytes( - bytes, - ivLength: 1, - messageLength: 3, - tagLength: 1, - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('Passed data is empty'), - ), - ), - ); - }); - - test('throws if one of the length is negative', () { - final Uint8List bytes = Uint8List(0); - expect( - () => CipherText.fromBytes( - bytes, - ivLength: -1, - messageLength: 1, - tagLength: 1, - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('Invalid length'), - ), - ), - ); - }); - }); - - group('get.cipherAlgorithm', () { - test('throws if not set', () { - final CipherText cipherText = CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ); - expect( - () => cipherText.cipherAlgorithm, - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_cipher', - ) - .having( - (e) => e.message, - 'message', - contains('Cipher algorithm is not specified'), - ), - ), - ); - }); - - test('returns the expected value', () { - final CipherText cipherText = CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - cipherAlgorithm: CipherAlgorithm.aes, - ); - expect(cipherText.cipherAlgorithm, CipherAlgorithm.aes); - }); - }); - - group('Lengths', () { - test('get.ivLength returns the expected value', () { - final CipherText cipherText = CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ); - expect(cipherText.ivLength, 1); - }); - - test('get.messageLength returns the expected value', () { - final CipherText cipherText = CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ); - expect(cipherText.messageLength, 1); - }); - - test('get.tagLength returns the expected value', () { - final CipherText cipherText = CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ); - expect(cipherText.tagLength, 1); - }); - }); -} diff --git a/packages/native_crypto/test/src/cipher_text_wrapper_test.dart b/packages/native_crypto/test/src/cipher_text_wrapper_test.dart deleted file mode 100644 index 8dc0116..0000000 --- a/packages/native_crypto/test/src/cipher_text_wrapper_test.dart +++ /dev/null @@ -1,349 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher_text_wrapper_test.dart -// Created Date: 26/05/2022 21:35:41 -// Last Modified: 27/05/2022 13:46:54 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto/native_crypto.dart'; - -void main() { - late CipherText single; - late List list; - - setUp(() { - Cipher.bytesCountPerChunk = Cipher.defaultBytesCountPerChunk; - single = CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ); - list = [ - CipherText.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ), - CipherText.fromBytes( - Uint8List.fromList([4, 5, 6]), - ivLength: 1, - messageLength: 1, - tagLength: 1, - ), - ]; - }); - - group('single', () { - test('makes isSingle true', () { - final wrapper = CipherTextWrapper.single(single); - expect(wrapper.isSingle, isTrue); - }); - - test('makes isList false', () { - final wrapper = CipherTextWrapper.single(single); - expect(wrapper.isList, isFalse); - }); - - test('makes CipherText the single value', () { - final wrapper = CipherTextWrapper.single(single); - expect(wrapper.single, single); - }); - - test('throws when trying to get list', () { - final wrapper = CipherTextWrapper.single(single); - expect( - () => wrapper.list, - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('is not list'), - ), - ), - ); - }); - - test('makes wrapper returns bytes of CipherText', () { - final wrapper = CipherTextWrapper.single(single); - expect(wrapper.bytes, single.bytes); - }); - - test('makes chunkCount = 1', () { - final wrapper = CipherTextWrapper.single(single); - expect(wrapper.chunkCount, 1); - }); - - test('makes unwrap() returns only CipherText', () { - final wrapper = CipherTextWrapper.single(single); - expect(wrapper.unwrap(), single); - }); - - test('makes unwrap() throws when trying to unwrap List', () { - final wrapper = CipherTextWrapper.single(single); - expect( - () => wrapper.unwrap>(), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('you should use unwrap'), - ), - ), - ); - }); - - test('makes adding is not supported', () { - final wrapper = CipherTextWrapper.single(single); - expect( - () => wrapper.add(single), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('is already single'), - ), - ), - ); - }); - }); - - group('list', () { - test('makes isList true', () { - final wrapper = CipherTextWrapper.list(list); - expect(wrapper.isList, isTrue); - }); - - test('makes isSingle false', () { - final wrapper = CipherTextWrapper.list(list); - expect(wrapper.isSingle, isFalse); - }); - - test('makes List the list value', () { - final wrapper = CipherTextWrapper.list(list); - expect(wrapper.list, list); - }); - - test('throws when trying to get single', () { - final wrapper = CipherTextWrapper.list(list); - expect( - () => wrapper.single, - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('is not single'), - ), - ), - ); - }); - - test('makes wrapper returns bytes of all CipherText joined', () { - final wrapper = CipherTextWrapper.list(list); - expect(wrapper.bytes, Uint8List.fromList([1, 2, 3, 4, 5, 6])); - }); - - test('makes chunkCount = 2', () { - final wrapper = CipherTextWrapper.list(list); - expect(wrapper.chunkCount, 2); - }); - - test('makes unwrap() returns List', () { - final wrapper = CipherTextWrapper.list(list); - expect(wrapper.unwrap>(), list); - }); - - test('makes unwrap() throws when trying to unwrap single', () { - final wrapper = CipherTextWrapper.list(list); - expect( - () => wrapper.unwrap(), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('you should use unwrap'), - ), - ), - ); - }); - - test('makes adding is supported', () { - final originalList = List.from(list); - final wrapper = CipherTextWrapper.list(list)..add(single); - printOnFailure(list.length.toString()); - expect(wrapper.list, [...originalList, single]); - }); - }); - - group('empty', () { - test('makes isList true', () { - final wrapper = CipherTextWrapper.empty(); - expect(wrapper.isList, isTrue); - }); - - test('makes isSingle false', () { - final wrapper = CipherTextWrapper.empty(); - expect(wrapper.isSingle, isFalse); - }); - - test('makes List the list value', () { - final wrapper = CipherTextWrapper.empty(); - expect(wrapper.list, []); - }); - - test('throws when trying to get single', () { - final wrapper = CipherTextWrapper.empty(); - expect( - () => wrapper.single, - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('is not single'), - ), - ), - ); - }); - - test('makes wrapper returns empty bytes', () { - final wrapper = CipherTextWrapper.empty(); - expect(wrapper.bytes, Uint8List.fromList([])); - }); - - test('makes chunkCount = 0', () { - final wrapper = CipherTextWrapper.empty(); - expect(wrapper.chunkCount, 0); - }); - - test('makes unwrap() returns empty List', () { - final wrapper = CipherTextWrapper.empty(); - expect(wrapper.unwrap>(), []); - }); - - test('makes unwrap() throws when trying to unwrap single', () { - final wrapper = CipherTextWrapper.empty(); - expect( - () => wrapper.unwrap(), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_data', - ) - .having( - (e) => e.message, - 'message', - contains('you should use unwrap'), - ), - ), - ); - }); - - test('makes adding is supported', () { - final wrapper = CipherTextWrapper.empty()..add(single); - expect(wrapper.list, [single]); - }); - }); - - group('fromBytes', () { - test('creates single from bytes when no too big', () { - final wrapper = CipherTextWrapper.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - tagLength: 1, - ); - expect(wrapper.isSingle, isTrue); - expect(wrapper.single, single); - }); - - test('creates list from bytes when too big', () { - Cipher.bytesCountPerChunk = 1; - final wrapper = CipherTextWrapper.fromBytes( - Uint8List.fromList([1, 2, 3, 4, 5, 6]), - ivLength: 1, - tagLength: 1, - ); - expect(wrapper.isList, isTrue); - expect(wrapper.list, list); - }); - - test('modifies Cipher.bytesCountPerChunk', () { - expect(Cipher.bytesCountPerChunk, Cipher.defaultBytesCountPerChunk); - CipherTextWrapper.fromBytes( - Uint8List.fromList([1, 2, 3]), - ivLength: 1, - tagLength: 1, - chunkSize: 3, - ); - expect(Cipher.bytesCountPerChunk, 3); - }); - - test('throws if trying to build list with bad parameters', () { - Cipher.bytesCountPerChunk = 1; // length of a message - - expect( - () => CipherTextWrapper.fromBytes( - Uint8List.fromList([1, 2, 3, 4, 5, 6]), - ivLength: 2, - tagLength: 1, - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('on chunk #'), - ), - ), - ); - }); - }); -} diff --git a/packages/native_crypto/test/src/digest_test.dart b/packages/native_crypto/test/src/digest_test.dart new file mode 100644 index 0000000..1927ea5 --- /dev/null +++ b/packages/native_crypto/test/src/digest_test.dart @@ -0,0 +1,116 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +import '../mocks/mock_native_crypto_api.dart'; + +void main() { + setUp(() { + // Mock the platform interface API + NativeCryptoPlatform.instance = BasicMessageChannelNativeCrypto( + api: MockNativeCryptoAPI(), + ); + }); + + group('Hash', () { + test('$Sha256 digest correctly', () async { + final hash = await Sha256().digest('abc'.toBytes()); + expect(hash, isNotNull); + expect( + hash, + Uint8List.fromList([1, 2, 3]), + ); + }); + + test('$Sha256 digest throws if platform returns null', () async { + MockNativeCryptoAPI.hashFn = (input, algorithm) => null; + expect( + () async => Sha256().digest('abc'.toBytes()), + throwsA(isA()), + ); + }); + + test('$Sha256 digest throws if platform returns invalid data', () async { + MockNativeCryptoAPI.hashFn = (input, algorithm) => Uint8List(0); + expect( + () async => Sha256().digest('abcd'.toBytes()), + throwsA(isA()), + ); + }); + + test('$Sha256 returns correct $HashAlgorithm', () async { + final hash = Sha256(); + + expect(hash.algorithm, HashAlgorithm.sha256); + }); + + test('$Sha384 returns correct $HashAlgorithm', () async { + final hash = Sha384(); + + expect(hash.algorithm, HashAlgorithm.sha384); + }); + + test('$Sha512 returns correct $HashAlgorithm', () async { + final hash = Sha512(); + + expect(hash.algorithm, HashAlgorithm.sha512); + }); + }); + + group('Hmac', () { + test('$HmacSha256 digest correctly', () async { + final hash = await HmacSha256() + .digest('abc'.toBytes(), SecretKey.fromUtf16('key')); + expect(hash, isNotNull); + expect( + hash, + Uint8List.fromList([1, 2, 3]), + ); + }); + + test('$HmacSha256 digest throws if platform returns null', () async { + MockNativeCryptoAPI.hmacFn = (input, key, algorithm) => null; + expect( + () async => + HmacSha256().digest('abc'.toBytes(), SecretKey.fromUtf16('key')), + throwsA(isA()), + ); + }); + + test('$HmacSha256 digest throws if platform returns invalid data', + () async { + MockNativeCryptoAPI.hmacFn = (input, key, algorithm) => Uint8List(0); + expect( + () async => + HmacSha256().digest('abc'.toBytes(), SecretKey.fromUtf16('key')), + throwsA(isA()), + ); + }); + + test('$HmacSha256 returns correct $HashAlgorithm', () async { + final hash = HmacSha256(); + + expect(hash.algorithm, HashAlgorithm.sha256); + }); + + test('$HmacSha384 returns correct $HashAlgorithm', () async { + final hash = HmacSha384(); + + expect(hash.algorithm, HashAlgorithm.sha384); + }); + + test('$HmacSha512 returns correct $HashAlgorithm', () async { + final hash = HmacSha512(); + + expect(hash.algorithm, HashAlgorithm.sha512); + }); + }); +} diff --git a/packages/native_crypto/test/src/hash_algorithm_test.dart b/packages/native_crypto/test/src/hash_algorithm_test.dart deleted file mode 100644 index b7911b1..0000000 --- a/packages/native_crypto/test/src/hash_algorithm_test.dart +++ /dev/null @@ -1,128 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: hash_algorithm_test.dart -// Created Date: 26/05/2022 22:28:53 -// Last Modified: 26/05/2022 23:03:03 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto/src/utils/hash_algorithm.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -import '../mocks/mock_native_crypto_platform.dart'; - -void main() { - final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); - NativeCryptoPlatform.instance = mock; - - group('name', () { - test('is sha256 for HashAlgorithm.sha256', () { - expect(HashAlgorithm.sha256.name, 'sha256'); - }); - test('is sha384 for HashAlgorithm.sha384', () { - expect(HashAlgorithm.sha384.name, 'sha384'); - }); - test('is sha512 for HashAlgorithm.sha512', () { - expect(HashAlgorithm.sha512.name, 'sha512'); - }); - }); - - group('digest', () { - test('handles returning empty list', () async { - mock - ..setDigestExpectations( - data: Uint8List.fromList([1, 2, 3]), - algorithm: 'sha256', - ) - ..setResponse(() => Uint8List(0)); - - await expectLater( - () => HashAlgorithm.sha256.digest(Uint8List.fromList([1, 2, 3])), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_empty_data', - ), - ), - ); - }); - - test('handles returning null', () async { - mock - ..setDigestExpectations( - data: Uint8List.fromList([1, 2, 3]), - algorithm: 'sha256', - ) - ..setResponse(() => null); - - await expectLater( - () => HashAlgorithm.sha256.digest(Uint8List.fromList([1, 2, 3])), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_null', - ), - ), - ); - }); - - test('handles throwing PlatformException', () async { - mock - ..setDigestExpectations( - data: Uint8List.fromList([1, 2, 3]), - algorithm: 'sha256', - ) - ..setResponse( - () => throw PlatformException( - code: 'native_crypto', - message: 'dummy error', - ), - ); - - await expectLater( - () => HashAlgorithm.sha256.digest(Uint8List.fromList([1, 2, 3])), - throwsA( - isA() - .having( - (e) => e.message, - 'message', - 'PlatformException(native_crypto, dummy error, null, null)', - ) - .having( - (e) => e.code, - 'code', - 'platform_throws', - ), - ), - ); - }); - - test('returns data on success', () async { - final hash = Uint8List.fromList([4, 5, 6]); - mock - ..setDigestExpectations( - data: Uint8List.fromList([1, 2, 3]), - algorithm: 'sha256', - ) - ..setResponse(() => hash); - - final result = await HashAlgorithm.sha256.digest( - Uint8List.fromList( - [1, 2, 3], - ), - ); - - expect( - result, - hash, - ); - }); - }); -} diff --git a/packages/native_crypto/test/src/pbkdf2_test.dart b/packages/native_crypto/test/src/pbkdf2_test.dart index f0ae084..9732018 100644 --- a/packages/native_crypto/test/src/pbkdf2_test.dart +++ b/packages/native_crypto/test/src/pbkdf2_test.dart @@ -1,280 +1,155 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: pbkdf2_test.dart -// Created Date: 26/05/2022 22:37:27 -// Last Modified: 26/05/2022 23:20:11 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. import 'dart:typed_data'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:native_crypto/native_crypto.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -import '../mocks/mock_native_crypto_platform.dart'; +import '../mocks/mock_native_crypto_api.dart'; void main() { - final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); - NativeCryptoPlatform.instance = mock; + setUp(() { + // Mock the platform interface API + NativeCryptoPlatform.instance = BasicMessageChannelNativeCrypto( + api: MockNativeCryptoAPI(), + ); - group('Constructor', () { - test('throws if keyBytesCount is negative', () { - expect( - () => Pbkdf2(keyBytesCount: -1, iterations: 10000), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('must be positive'), - ), - ), - ); - }); - - test('throws if iterations is negative or 0', () { - expect( - () => Pbkdf2(keyBytesCount: 32, iterations: -1), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('must be strictly positive'), - ), - ), - ); - }); + MockNativeCryptoAPI.pbkdf2Fn = null; }); - group('derive', () { - test('throws if password is null', () async { - final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); - await expectLater( - () => pbkdf2.derive( - salt: 'salt', - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('cannot be null'), - ), - ), - ); - }); - - test('throws if salt is null', () async { - final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); - await expectLater( - () => pbkdf2.derive( - password: 'password', - ), - throwsA( - isA() - .having( - (e) => e.code, - 'code', - 'invalid_argument', - ) - .having( - (e) => e.message, - 'message', - contains('cannot be null'), - ), - ), - ); - }); - - test('handles returning empty list', () async { - mock - ..setPbkdf2Expectations( - password: 'password', - salt: 'salt', - keyBytesCount: 32, - iterations: 10000, - algorithm: 'sha256', - ) - ..setResponse(() => Uint8List(0)); - - final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); - - await expectLater( - () => pbkdf2.derive( - password: 'password', - salt: 'salt', - ), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_empty_data', - ), - ), - ); - }); - - test('handles returning null', () async { - mock - ..setPbkdf2Expectations( - password: 'password', - salt: 'salt', - keyBytesCount: 32, - iterations: 10000, - algorithm: 'sha256', - ) - ..setResponse(() => null); - - final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); - - await expectLater( - () => pbkdf2.derive( - password: 'password', - salt: 'salt', - ), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_null', - ), - ), - ); - }); - - test('handles returning data with wrong length', () async { - mock - ..setPbkdf2Expectations( - password: 'password', - salt: 'salt', - keyBytesCount: 32, - iterations: 10000, - algorithm: 'sha256', - ) - ..setResponse(() => Uint8List(33)); - - final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); - - await expectLater( - () => pbkdf2.derive( - password: 'password', - salt: 'salt', - ), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_invalid_data', - ), - ), - ); - }); - - test('handles throwing PlatformException', () async { - mock - ..setPbkdf2Expectations( - password: 'password', - salt: 'salt', - keyBytesCount: 32, - iterations: 10000, - algorithm: 'sha256', - ) - ..setResponse( - () => throw PlatformException( - code: 'native_crypto', - message: 'dummy error', - ), - ); - - final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); - - await expectLater( - () => pbkdf2.derive( - password: 'password', - salt: 'salt', - ), - throwsA( - isA() - .having( - (e) => e.message, - 'message', - 'PlatformException(native_crypto, dummy error, null, null)', - ) - .having( - (e) => e.code, - 'code', - 'platform_throws', - ), - ), - ); - }); - - test('returns SecretKey on success', () async { - final data = Uint8List.fromList([1, 2, 3, 4, 5, 6]); - final sk = SecretKey(data); - mock - ..setPbkdf2Expectations( - password: 'password', - salt: 'salt', - keyBytesCount: 6, - iterations: 10000, - algorithm: 'sha256', - ) - ..setResponse(() => data); - - final pbkdf = Pbkdf2(keyBytesCount: 6, iterations: 10000); - final result = await pbkdf.derive( - password: 'password', - salt: 'salt', + group('$Pbkdf2', () { + test('derive key correctly', () async { + final key = await Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 3, + hashAlgorithm: HashAlgorithm.sha256, + ).derive( + Uint8List.fromList([1, 2, 3]), ); + expect(key, isNotNull); + expect(key.length, 3); expect( - result, - sk, + key, + Uint8List.fromList([1, 2, 3]), ); }); - test('return empty SecretKey when keyBytesCount is set to 0', () async { - final sk = SecretKey(Uint8List(0)); - mock - ..setPbkdf2Expectations( - password: 'password', - salt: 'salt', - keyBytesCount: 0, - iterations: 10000, - algorithm: 'sha256', - ) - ..setResponse(() => Uint8List(0)); - - final pbkdf = Pbkdf2(keyBytesCount: 0, iterations: 10000); - final result = await pbkdf.derive( - password: 'password', - salt: 'salt', - ); - + test('derive key with invalid length throws', () async { expect( - result, - sk, + () => Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: -1, + hashAlgorithm: HashAlgorithm.sha256, + ).derive( + Uint8List.fromList([1, 2, 3]), + ), + throwsA(isA()), ); }); + + test('derive key with invalid iterations throws', () async { + expect( + () => Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 0, + length: 3, + hashAlgorithm: HashAlgorithm.sha256, + ).derive( + Uint8List.fromList([1, 2, 3]), + ), + throwsA(isA()), + ); + }); + + test('derive key with 0 length returns empty list', () async { + final key = await Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 0, + hashAlgorithm: HashAlgorithm.sha256, + ).derive( + Uint8List.fromList([1, 2, 3]), + ); + expect(key, isNotNull); + expect(key.length, 0); + }); + + test('derive key throws if platform returns null', () async { + MockNativeCryptoAPI.pbkdf2Fn = + (password, salt, iterations, length, hashAlgorithm) => null; + expect( + () => Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 3, + hashAlgorithm: HashAlgorithm.sha256, + ).derive( + Uint8List.fromList([1, 2, 3]), + ), + throwsA(isA()), + ); + }); + + test('derive key throws if platform returns invalid data', () async { + expect( + () async => Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 4, + hashAlgorithm: HashAlgorithm.sha256, + ).derive( + Uint8List.fromList([1, 2, 3]), + ), + throwsA(isA()), + ); + }); + + test('call returns $SecretKey', () async { + final pbkdf = Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 3, + hashAlgorithm: HashAlgorithm.sha256, + ); + + final key = await pbkdf(password: 'password'); + + expect(key, isNotNull); + expect(key, isA()); + }); + + test('verify key returns true on the same password', () async { + final pbkdf = Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 3, + hashAlgorithm: HashAlgorithm.sha256, + ); + final pwd = Uint8List.fromList([1, 2, 3]); + final key = await pbkdf.derive(pwd); + final sucess = await pbkdf.verify(pwd, key); + expect(sucess, true); + }); + + test('verify key returns true on the same password', () async { + final pbkdf = Pbkdf2( + salt: Uint8List.fromList([1, 2, 3]), + iterations: 1, + length: 3, + hashAlgorithm: HashAlgorithm.sha256, + ); + final pwd = Uint8List.fromList([1, 2, 3]); + final key = Uint8List.fromList([1, 2, 3, 4, 5, 6]); + final sucess = await pbkdf.verify(pwd, key); + expect(sucess, false); + }); }); } diff --git a/packages/native_crypto/test/src/random_test.dart b/packages/native_crypto/test/src/random_test.dart new file mode 100644 index 0000000..71fb794 --- /dev/null +++ b/packages/native_crypto/test/src/random_test.dart @@ -0,0 +1,63 @@ +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +import '../mocks/mock_native_crypto_api.dart'; + +void main() { + setUp(() { + // Mock the platform interface API + NativeCryptoPlatform.instance = BasicMessageChannelNativeCrypto( + api: MockNativeCryptoAPI(), + ); + }); + + group('$SecureRandom', () { + test('generate random bytes correctly', () async { + final random = await const SecureRandom().generate(3); + expect(random, isNotNull); + expect(random.length, 3); + expect( + random, + Uint8List.fromList([1, 2, 3]), + ); + }); + + test('generate random bytes with invalid length throws', () async { + expect( + () => const SecureRandom().generate(-1), + throwsA(isA()), + ); + }); + + test('generate random bytes with 0 length returns empty list', () async { + final random = await const SecureRandom().generate(0); + expect(random, isNotNull); + expect(random.length, 0); + }); + + test('generate random bytes throws if platform returns null', () async { + MockNativeCryptoAPI.generateSecureRandomFn = (length) => null; + expect( + () async => const SecureRandom().generate(3), + throwsA(isA()), + ); + }); + + test('generate random bytes throws if platform returns invalid data', + () async { + expect( + () async => const SecureRandom().generate(4), + throwsA(isA()), + ); + }); + }); +} diff --git a/packages/native_crypto/test/src/secret_key_test.dart b/packages/native_crypto/test/src/secret_key_test.dart index df2f159..29716fe 100644 --- a/packages/native_crypto/test/src/secret_key_test.dart +++ b/packages/native_crypto/test/src/secret_key_test.dart @@ -1,125 +1,56 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: secret_key_test.dart -// Created Date: 26/05/2022 10:52:41 -// Last Modified: 26/05/2022 22:38:07 -// ----- -// Copyright (c) 2022 +// Copyright 2019-2023 Hugo Pointcheval +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. import 'dart:typed_data'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto/native_crypto.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -import '../mocks/mock_native_crypto_platform.dart'; +import '../mocks/mock_native_crypto_api.dart'; void main() { - final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); - NativeCryptoPlatform.instance = mock; + setUp(() { + // Mock the platform interface API + NativeCryptoPlatform.instance = BasicMessageChannelNativeCrypto( + api: MockNativeCryptoAPI(), + ); + }); - group('Constructors', () { - test('handles Uint8List', () { + group('$SecretKey', () { + test('can be create from Uint8List', () { final SecretKey key = SecretKey(Uint8List.fromList([1, 2, 3, 4, 5])); expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); }); - test('handles base16', () { + test('can be create from base16', () { final SecretKey key = SecretKey.fromBase16('0102030405'); expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); }); - test('handles base64', () { + test('can be create from base64', () { final SecretKey key = SecretKey.fromBase64('AQIDBAU='); expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); }); - test('handles utf8', () { + test('can be create from utf8', () { final SecretKey key = SecretKey.fromUtf8('ABCDE'); expect(key.bytes, Uint8List.fromList([65, 66, 67, 68, 69])); }); - }); - group('fromSecureRandom', () { - test('handles returning random bytes', () async { - mock - ..setGenerateKeyExpectations(bitsCount: 5) - ..setResponse(() => Uint8List.fromList([1, 2, 3, 4, 5])); + test('can be create from secure random', () async { + MockNativeCryptoAPI.generateSecureRandomFn = + (length) => Uint8List.fromList([1, 2, 3, 4, 5]); + final SecretKey key = await SecretKey.fromSecureRandom(5); - final SecretKey secretKey = await SecretKey.fromSecureRandom(5); - - expect( - secretKey.bytes, - Uint8List.fromList([1, 2, 3, 4, 5]), - ); - }); - - test('handles returning empty list', () async { - mock - ..setGenerateKeyExpectations(bitsCount: 5) - ..setResponse(() => Uint8List(0)); - - await expectLater( - () => SecretKey.fromSecureRandom(5), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_empty_data', - ), - ), - ); - }); - - test('handles returning null', () async { - mock - ..setGenerateKeyExpectations(bitsCount: 5) - ..setResponse(() => null); - - await expectLater( - () => SecretKey.fromSecureRandom(5), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'platform_returned_null', - ), - ), - ); - }); - - test('handles throwing PlatformException', () async { - mock - ..setGenerateKeyExpectations(bitsCount: 5) - ..setResponse( - () => throw PlatformException( - code: 'native_crypto', - message: 'dummy error', - ), - ); - - await expectLater( - () => SecretKey.fromSecureRandom(5), - throwsA( - isA() - .having( - (e) => e.message, - 'message', - 'PlatformException(native_crypto, dummy error, null, null)', - ) - .having( - (e) => e.code, - 'code', - 'platform_throws', - ), - ), - ); + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); }); }); }