From 0ee953ab107cebfd4a5feb8a2ac108e594e18a29 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Wed, 16 Oct 2024 15:59:12 +0530 Subject: [PATCH 1/6] feat: Secure atKeys with pass-phrase --- packages/at_auth/lib/src/at_auth_impl.dart | 53 +++++--- .../at_auth/lib/src/auth/at_auth_request.dart | 3 + .../at_auth/lib/src/keys/at_auth_keys.dart | 1 + packages/at_chops/lib/at_chops.dart | 28 ++-- .../src/algorithm/aes_encryption_algo.dart | 73 +++++++++- .../at_chops/lib/src/algorithm/algo_type.dart | 24 +++- .../src/algorithm/argon2id_hashing_algo.dart | 52 +++++++ .../lib/src/algorithm/at_algorithm.dart | 23 ++-- .../src/algorithm/default_hashing_algo.dart | 5 +- packages/at_chops/lib/src/at_chops_impl.dart | 9 +- packages/at_chops/lib/src/at_keys_crypto.dart | 120 +++++++++++++++++ .../at_chops/lib/src/model/at_encrypted.dart | 68 ++++++++++ .../at_chops/lib/src/model/hash_params.dart | 36 +++++ packages/at_chops/pubspec.yaml | 1 + .../at_chops/test/at_keys_crypto_test.dart | 52 +++++++ packages/at_cli_commons/lib/src/cli_base.dart | 42 +++--- .../lib/src/cli/auth_cli.dart | 28 ++-- .../lib/src/cli/auth_cli_args.dart | 15 ++- .../onboard/at_onboarding_service_impl.dart | 48 ++----- .../src/util/at_onboarding_preference.dart | 2 + packages/at_onboarding_cli/pubspec.yaml | 7 + .../test/at_onboarding_cli_test.dart | 13 +- .../test/enrollment_cli_commands_test.dart | 127 ++++++++++++++---- 23 files changed, 689 insertions(+), 141 deletions(-) create mode 100644 packages/at_chops/lib/src/algorithm/argon2id_hashing_algo.dart create mode 100644 packages/at_chops/lib/src/at_keys_crypto.dart create mode 100644 packages/at_chops/lib/src/model/at_encrypted.dart create mode 100644 packages/at_chops/lib/src/model/hash_params.dart create mode 100644 packages/at_chops/test/at_keys_crypto_test.dart diff --git a/packages/at_auth/lib/src/at_auth_impl.dart b/packages/at_auth/lib/src/at_auth_impl.dart index 3861137d..9bfb087d 100644 --- a/packages/at_auth/lib/src/at_auth_impl.dart +++ b/packages/at_auth/lib/src/at_auth_impl.dart @@ -49,11 +49,9 @@ class AtAuthImpl implements AtAuth { AtAuthKeys? atAuthKeys; var enrollmentIdFromRequest = atAuthRequest.enrollmentId; if (atAuthRequest.atKeysFilePath != null) { - atAuthKeys = _decryptAtKeysFile( - await _readAtKeysFile(atAuthRequest.atKeysFilePath), - atAuthRequest.authMode); + atAuthKeys = await _prepareAtAuthKeysFromFilePath(atAuthRequest); } else if (atAuthRequest.encryptedKeysMap != null) { - atAuthKeys = _decryptAtKeysFile( + atAuthKeys = _decryptAtKeysWithSelfEncKey( atAuthRequest.encryptedKeysMap!, PkamAuthMode.keysFile); } else { atAuthKeys = atAuthRequest.atAuthKeys; @@ -247,7 +245,7 @@ class AtAuthImpl implements AtAuth { return enrollmentIdFromServer!; } - AtAuthKeys _decryptAtKeysFile( + AtAuthKeys _decryptAtKeysWithSelfEncKey( Map jsonData, PkamAuthMode authMode) { var securityKeys = AtAuthKeys(); String decryptionKey = jsonData[auth_constants.defaultSelfEncryptionKey]!; @@ -285,21 +283,46 @@ class AtAuthImpl implements AtAuth { ///method to read and return data from .atKeysFile ///returns map containing encryption keys - Future> _readAtKeysFile(String? atKeysFilePath) async { - if (atKeysFilePath == null || atKeysFilePath.isEmpty) { + Future _prepareAtAuthKeysFromFilePath( + AtAuthRequest atAuthRequest) async { + if (atAuthRequest.atKeysFilePath == null || + atAuthRequest.atKeysFilePath!.isEmpty) { throw AtException( 'atKeys filePath is empty. atKeysFile is required to authenticate'); } - if (!File(atKeysFilePath).existsSync()) { + if (!File(atAuthRequest.atKeysFilePath!).existsSync()) { throw AtException( - 'provided keys file does not exist. Please check whether the file path $atKeysFilePath is valid'); + 'provided keys file does not exist. Please check whether the file path ${atAuthRequest.atKeysFilePath} is valid'); } - String atAuthData = await File(atKeysFilePath).readAsString(); - Map jsonData = {}; - json.decode(atAuthData).forEach((String key, dynamic value) { - jsonData[key] = value.toString(); - }); - return jsonData; + + String atAuthData = + await File(atAuthRequest.atKeysFilePath!).readAsString(); + Map decodedAtKeysData = jsonDecode(atAuthData); + // If it contains "iv(InitializationVector)", it means the data is encrypted with a + // passphrase. Decrypt it. + if (decodedAtKeysData.containsKey('iv') && + atAuthRequest.passPhrase.isNullOrEmpty) { + throw AtDecryptionException( + 'Pass Phrase is required for password protected atKeys file'); + } + if (decodedAtKeysData.containsKey('iv')) { + _logger.info( + 'Found encrypted atKeys files. Decrypting with the given pass-phrase'); + AtEncrypted atEncrypted = AtEncrypted.fromJson(decodedAtKeysData); + + if (atEncrypted.hashingAlgoType == null) { + throw AtDecryptionException( + 'Hashing algo type is required for decryption of AtKeys file'); + } + + String decryptedAtKeys = + await AtKeysCrypto.fromHashingAlgorithm(atEncrypted.hashingAlgoType!) + .decrypt(atEncrypted, atAuthRequest.passPhrase!); + decodedAtKeysData = jsonDecode(decryptedAtKeys); + } + // This is to decrypt the atKeys encrypted with self Encryption key. + return _decryptAtKeysWithSelfEncKey( + decodedAtKeysData, atAuthRequest.authMode); } AtAuthKeys _generateKeyPairs(PkamAuthMode authMode, {String? publicKeyId}) { diff --git a/packages/at_auth/lib/src/auth/at_auth_request.dart b/packages/at_auth/lib/src/auth/at_auth_request.dart index ca473494..658b5406 100644 --- a/packages/at_auth/lib/src/auth/at_auth_request.dart +++ b/packages/at_auth/lib/src/auth/at_auth_request.dart @@ -38,4 +38,7 @@ class AtAuthRequest { /// Hashing algorithm to use for pkam authentication HashingAlgoType hashingAlgoType = HashingAlgoType.sha256; + + /// The pass phrase to password protect the AtKeys file. + String? passPhrase; } diff --git a/packages/at_auth/lib/src/keys/at_auth_keys.dart b/packages/at_auth/lib/src/keys/at_auth_keys.dart index f69f18ea..3d78e806 100644 --- a/packages/at_auth/lib/src/keys/at_auth_keys.dart +++ b/packages/at_auth/lib/src/keys/at_auth_keys.dart @@ -14,6 +14,7 @@ class AtAuthKeys { AtAuthKeys(); + @Deprecated('Use toJson()') Map toMap() { var keysMap = {}; keysMap[auth_constants.apkamPrivateKey] = apkamPrivateKey; diff --git a/packages/at_chops/lib/at_chops.dart b/packages/at_chops/lib/at_chops.dart index 3689dd60..222bf389 100644 --- a/packages/at_chops/lib/at_chops.dart +++ b/packages/at_chops/lib/at_chops.dart @@ -1,25 +1,31 @@ library at_chops; +export 'src/algorithm/aes_encryption_algo.dart'; +export 'src/algorithm/algo_type.dart'; +export 'src/algorithm/at_iv.dart'; +export 'src/algorithm/default_signing_algo.dart'; +export 'src/algorithm/ecc_signing_algo.dart'; +export 'src/algorithm/pkam_signing_algo.dart'; +export 'src/algorithm/rsa_encryption_algo.dart'; export 'src/at_chops_base.dart'; export 'src/at_chops_impl.dart'; +// Class to encrypt/decrypt atKeys file based on the password specified. +export 'src/at_keys_crypto.dart'; +export 'src/key/at_key_pair.dart'; +export 'src/key/at_private_key.dart'; +export 'src/key/at_public_key.dart'; +export 'src/key/impl/aes_key.dart'; export 'src/key/impl/at_chops_keys.dart'; export 'src/key/impl/at_encryption_key_pair.dart'; export 'src/key/impl/at_pkam_key_pair.dart'; -export 'src/key/impl/aes_key.dart'; export 'src/key/key_type.dart'; export 'src/metadata/at_signing_input.dart'; export 'src/metadata/encryption_metadata.dart'; export 'src/metadata/encryption_result.dart'; export 'src/metadata/signing_metadata.dart'; export 'src/metadata/signing_result.dart'; +// A model class which represents the encrypted AtKeys with a passphrase. +export 'src/model/at_encrypted.dart'; +// Class representing the hashing parameters to pass to an hashing algorithm. +export 'src/model/hash_params.dart' hide HashParams; export 'src/util/at_chops_util.dart'; -export 'src/algorithm/algo_type.dart'; -export 'src/algorithm/at_iv.dart'; -export 'src/algorithm/aes_encryption_algo.dart'; -export 'src/algorithm/rsa_encryption_algo.dart'; -export 'src/algorithm/default_signing_algo.dart'; -export 'src/algorithm/pkam_signing_algo.dart'; -export 'src/algorithm/ecc_signing_algo.dart'; -export 'src/key/at_key_pair.dart'; -export 'src/key/at_public_key.dart'; -export 'src/key/at_private_key.dart'; diff --git a/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart b/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart index 32d0f8c9..f512e8ca 100644 --- a/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart +++ b/packages/at_chops/lib/src/algorithm/aes_encryption_algo.dart @@ -1,12 +1,16 @@ import 'dart:typed_data'; +import 'package:at_chops/at_chops.dart'; import 'package:at_chops/src/algorithm/at_algorithm.dart'; -import 'package:at_chops/src/algorithm/at_iv.dart'; -import 'package:at_chops/src/key/impl/aes_key.dart'; +import 'package:at_commons/at_commons.dart'; import 'package:encrypt/encrypt.dart'; -class AESEncryptionAlgo implements SymmetricEncryptionAlgorithm { +/// A class that provides AES encryption and decryption for Uint8List, +/// implementing the [SymmetricEncryptionAlgorithm] interface. +class AESEncryptionAlgo + implements SymmetricEncryptionAlgorithm { final AESKey _aesKey; + AESEncryptionAlgo(this._aesKey); @override @@ -33,3 +37,66 @@ class AESEncryptionAlgo implements SymmetricEncryptionAlgorithm { return IV(Uint8List(16)); } } + +/// A class that provides AES encryption and decryption for strings, +/// implementing the [SymmetricEncryptionAlgorithm] interface. +/// +/// This class uses an [AESKey] to perform encryption and decryption of strings. +/// The key and an [InitialisationVector] (IV) are used for encryption, and the +/// same key must be used for decryption. +class StringAESEncryptor + implements SymmetricEncryptionAlgorithm { + /// The AES key used for encryption and decryption. + final AESKey _aesKey; + + /// Constructs an instance of [StringAESEncryptor] with the provided [_aesKey]. + /// + /// [_aesKey]: The key used for AES encryption and decryption, represented + /// in Base64 format. + StringAESEncryptor(this._aesKey); + + /// Decrypts the given [encryptedData] using the provided [iv] (Initialisation Vector). + /// + /// The [iv] used for encryption must be the same for decryption. If [iv] is + /// not provided, an [AtDecryptionException] will be thrown, as the IV is + /// mandatory for the AES decryption process. + /// + /// - [encryptedData]: The Base64-encoded string that represents the encrypted data. + /// - [iv]: The Initialisation Vector used during encryption. Must be the same + /// IV that was used to encrypt the data. + /// + /// Returns a [String] that represents the decrypted data. + /// + /// Throws an [AtDecryptionException] if the [iv] is missing. + + @override + String decrypt(String encryptedData, {InitialisationVector? iv}) { + // The IV used for encryption, the same IV must be used for decryption. + if (iv == null) { + throw AtDecryptionException( + 'Initialisation Vector (IV) is required for decryption'); + } + var aesEncrypter = Encrypter(AES(Key.fromBase64(_aesKey.key))); + return aesEncrypter.decrypt(Encrypted.fromBase64(encryptedData), + iv: IV(iv.ivBytes)); + } + + /// Encrypts the given [plainData] using AES encryption and an optional [iv]. + /// + /// If no [iv] is provided, a random 16-byte IV will be generated using + /// [AtChopsUtil.generateRandomIV]. The resulting encrypted data will be + /// Base64-encoded. + /// + /// - [plainData]: The string that needs to be encrypted. + /// - [iv]: The Initialisation Vector used for encryption. If not provided, + /// a random 16-byte IV will be generated. + /// + /// Returns a [String] that contains the encrypted data, encoded in Base64 format. + @override + String encrypt(String plainData, {InitialisationVector? iv}) { + iv ??= AtChopsUtil.generateRandomIV(16); + var aesEncrypter = Encrypter(AES(Key.fromBase64(_aesKey.key))); + final encrypted = aesEncrypter.encrypt(plainData, iv: IV(iv.ivBytes)); + return encrypted.base64; + } +} diff --git a/packages/at_chops/lib/src/algorithm/algo_type.dart b/packages/at_chops/lib/src/algorithm/algo_type.dart index 83dcb0f4..452e4faf 100644 --- a/packages/at_chops/lib/src/algorithm/algo_type.dart +++ b/packages/at_chops/lib/src/algorithm/algo_type.dart @@ -1,4 +1,26 @@ // ignore: constant_identifier_names +import 'package:at_commons/at_commons.dart'; + enum SigningAlgoType { ecc_secp256r1, rsa2048, rsa4096 } -enum HashingAlgoType { sha256, sha512, md5 } +enum HashingAlgoType { + sha256, + sha512, + md5, + argon2id; + + static HashingAlgoType fromString(String name) { + switch (name.toLowerCase()) { + case 'sha256': + return HashingAlgoType.sha256; + case 'sha512': + return HashingAlgoType.sha512; + case 'md5': + return HashingAlgoType.md5; + case 'argon2id': + return HashingAlgoType.argon2id; + default: + throw AtException('Invalid hashing algo type'); + } + } +} diff --git a/packages/at_chops/lib/src/algorithm/argon2id_hashing_algo.dart b/packages/at_chops/lib/src/algorithm/argon2id_hashing_algo.dart new file mode 100644 index 00000000..567ab995 --- /dev/null +++ b/packages/at_chops/lib/src/algorithm/argon2id_hashing_algo.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:at_chops/src/algorithm/at_algorithm.dart'; +import 'package:at_chops/src/model/hash_params.dart'; +import 'package:cryptography/cryptography.dart'; + +/// A class that implements the Argon2id hashing algorithm for password hashing. +/// +/// This class provides a method to hash a given password using the Argon2id +/// algorithm, which is a memory-hard, CPU-intensive key derivation function +/// suitable for password hashing and encryption key derivation. +/// +/// The class uses the `cryptography` package's `Argon2id` algorithm for deriving +/// a key from a password and encodes the result into a Base64 string. +class Argon2idHashingAlgo implements AtHashingAlgorithm { + /// Hashes a given password using the Argon2id algorithm. + /// + /// The [password] parameter is required, and it represents the password or + /// passphrase to be hashed. + /// + /// The [hashParams] parameter is optional. It allows customizing the Argon2id + /// parameters, such as: + /// - [HashParams.parallelism]: The degree of parallelism (threads) to use. + /// - [HashParams.memory]: The amount of memory (in KB) to use. + /// - [HashParams.iterations]: The number of iterations (time cost) to apply. + /// - [HashParams.hashLength]: The length of the resulting hash (in bytes). + /// + /// If [hashParams] is not provided, default values will be used. + /// + /// The method returns a [Future] that resolves to a Base64-encoded string + /// representing the hashed value of the input password. + /// + /// Throws: + /// - [ArgumentError] if the provided password is null or empty. + /// + /// Returns a Base64-encoded string representing the derived key. + @override + Future hash(String password, {ArgonHashParams? hashParams}) async { + hashParams ??= ArgonHashParams(); + final argon2id = Argon2id( + parallelism: hashParams.parallelism, + memory: hashParams.memory, + iterations: hashParams.iterations, + hashLength: hashParams.hashLength); + + SecretKey secretKey = await argon2id.deriveKeyFromPassword( + password: password, nonce: password.codeUnits); + + return Base64Encoder().convert(await secretKey.extractBytes()); + } +} diff --git a/packages/at_chops/lib/src/algorithm/at_algorithm.dart b/packages/at_chops/lib/src/algorithm/at_algorithm.dart index 62588a3f..f41a6d71 100644 --- a/packages/at_chops/lib/src/algorithm/at_algorithm.dart +++ b/packages/at_chops/lib/src/algorithm/at_algorithm.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; @@ -5,26 +6,30 @@ import 'package:at_chops/src/algorithm/at_iv.dart'; import 'package:at_chops/src/key/at_key_pair.dart'; import 'package:at_chops/src/key/at_private_key.dart'; import 'package:at_chops/src/key/at_public_key.dart'; +import 'package:at_chops/src/model/hash_params.dart'; /// Interface for encrypting and decrypting data. Check [DefaultEncryptionAlgo] for sample implementation. -abstract class AtEncryptionAlgorithm { +abstract class AtEncryptionAlgorithm { /// Encrypts the passed bytes. Bytes are passed as [Uint8List]. Encode String data type to [Uint8List] using [utf8.encode]. - Uint8List encrypt(Uint8List plainData); + V encrypt(T plainData); /// Decrypts the passed encrypted bytes. - Uint8List decrypt(Uint8List encryptedData); + V decrypt(T encryptedData); } /// Interface for symmetric encryption algorithms. Check [AESEncryptionAlgo] for sample implementation. -abstract class SymmetricEncryptionAlgorithm extends AtEncryptionAlgorithm { +abstract class SymmetricEncryptionAlgorithm + extends AtEncryptionAlgorithm { @override - Uint8List encrypt(Uint8List plainData, {InitialisationVector iv}); + V encrypt(T plainData, {InitialisationVector iv}); + @override - Uint8List decrypt(Uint8List encryptedData, {InitialisationVector iv}); + V decrypt(T encryptedData, {InitialisationVector iv}); } /// Interface for asymmetric encryption algorithms. Check [DefaultEncryptionAlgo] for sample implementation. -abstract class ASymmetricEncryptionAlgorithm extends AtEncryptionAlgorithm { +abstract class ASymmetricEncryptionAlgorithm + extends AtEncryptionAlgorithm { AtPublicKey? atPublicKey; AtPrivateKey? atPrivateKey; @@ -48,7 +53,7 @@ abstract class AtSigningAlgorithm { } /// Interface for hashing data. Refer [DefaultHash] for sample implementation. -abstract class AtHashingAlgorithm { +abstract class AtHashingAlgorithm { /// Hashes the passed data - String hash(Uint8List data); + FutureOr hash(K data, {covariant HashParams? hashParams}); } diff --git a/packages/at_chops/lib/src/algorithm/default_hashing_algo.dart b/packages/at_chops/lib/src/algorithm/default_hashing_algo.dart index eb057a9d..b1d62083 100644 --- a/packages/at_chops/lib/src/algorithm/default_hashing_algo.dart +++ b/packages/at_chops/lib/src/algorithm/default_hashing_algo.dart @@ -1,9 +1,10 @@ import 'package:at_chops/src/algorithm/at_algorithm.dart'; +import 'package:at_chops/src/model/hash_params.dart'; import 'package:crypto/crypto.dart'; -class DefaultHash implements AtHashingAlgorithm { +class DefaultHash implements AtHashingAlgorithm, String> { @override - String hash(List data) { + String hash(List data, {HashParams? hashParams}) { return md5.convert(data).toString(); } } diff --git a/packages/at_chops/lib/src/at_chops_impl.dart b/packages/at_chops/lib/src/at_chops_impl.dart index 67d33daa..55b18450 100644 --- a/packages/at_chops/lib/src/at_chops_impl.dart +++ b/packages/at_chops/lib/src/at_chops_impl.dart @@ -7,10 +7,10 @@ import 'package:at_chops/src/algorithm/aes_encryption_algo.dart'; import 'package:at_chops/src/algorithm/algo_type.dart'; import 'package:at_chops/src/algorithm/at_algorithm.dart'; import 'package:at_chops/src/algorithm/at_iv.dart'; -import 'package:at_chops/src/algorithm/rsa_encryption_algo.dart'; import 'package:at_chops/src/algorithm/default_signing_algo.dart'; import 'package:at_chops/src/algorithm/ecc_signing_algo.dart'; import 'package:at_chops/src/algorithm/pkam_signing_algo.dart'; +import 'package:at_chops/src/algorithm/rsa_encryption_algo.dart'; import 'package:at_chops/src/at_chops_base.dart'; import 'package:at_chops/src/key/at_key_pair.dart'; import 'package:at_chops/src/key/impl/aes_key.dart'; @@ -25,6 +25,8 @@ import 'package:at_chops/src/metadata/signing_result.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_utils/at_logger.dart'; +import 'algorithm/default_hashing_algo.dart'; + class AtChopsImpl extends AtChops { AtChopsImpl(super.atChopsKeys); @@ -143,7 +145,10 @@ class AtChopsImpl extends AtChops { @override String hash(Uint8List signedData, AtHashingAlgorithm hashingAlgorithm) { - return hashingAlgorithm.hash(signedData); + if (hashingAlgorithm.runtimeType == DefaultHash) { + return DefaultHash().hash(signedData); + } + throw AtException('$hashingAlgorithm is not supported'); } @override diff --git a/packages/at_chops/lib/src/at_keys_crypto.dart b/packages/at_chops/lib/src/at_keys_crypto.dart new file mode 100644 index 00000000..39b6fa05 --- /dev/null +++ b/packages/at_chops/lib/src/at_keys_crypto.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:at_chops/at_chops.dart'; +import 'package:at_chops/src/algorithm/argon2id_hashing_algo.dart'; +import 'package:at_chops/src/algorithm/at_algorithm.dart'; +import 'package:at_chops/src/algorithm/default_hashing_algo.dart'; +import 'package:at_chops/src/model/hash_params.dart'; +import 'package:at_commons/at_commons.dart'; + +/// An abstract class that provides cryptographic operations for AtKeys using +/// specific hashing algorithms. +/// +/// This class allows for encryption and decryption of AtKeys +/// with a passphrase, using the provided hashing algorithm type. +abstract class AtKeysCrypto { + /// Returns an instance of [_AtKeysCryptoImpl] based on the + /// provided [hashingAlgoType]. + /// + /// The [hashingAlgoType] parameter determines the hashing + /// algorithm to be used in the cryptographic operations. + static AtKeysCrypto fromHashingAlgorithm(HashingAlgoType hashingAlgoType) => + _AtKeysCryptoImpl(hashingAlgoType); + + /// Encrypts the given [plainAtKeys] using the provided [passPhrase] and + /// optional [hashParams]. + /// + /// This method returns an [AtEncrypted] object, which contains the encrypted + /// AtKeys. + /// + /// - [plainAtKeys]: The plain text AtKeys to be encrypted. + /// - [passPhrase]: The passphrase used for encryption. + /// - [hashParams]: Optional parameters used for hashing in the + /// encryption process. + /// + /// Returns a [FutureOr] that resolves to [AtEncrypted] on success. + FutureOr encrypt(String plainAtKeys, String passPhrase, + {HashParams? hashParams}); + + /// Decrypts the given [atEncrypted] object back to its original + /// plain text format using the provided [passPhrase] and + /// optional [hashParams]. + /// + /// - [atEncrypted]: The encrypted AtKeys object to be decrypted. + /// - [passPhrase]: The passphrase used for decryption. + /// - [hashParams]: Optional parameters used for hashing in the + /// decryption process. + /// + /// Returns a [FutureOr] that resolves to a [String] containing + /// the decrypted AtKeys on success. + FutureOr decrypt(AtEncrypted atEncrypted, String passPhrase, + {HashParams? hashParams}); +} + +/// The implementation class of [AtKeysCrypto]. The implementation classes is marked private. +/// Use [AtKeysCrypto.fromHashingAlgorithm] to get an instance of [_AtKeysCryptoImpl]. +class _AtKeysCryptoImpl implements AtKeysCrypto { + final HashingAlgoType _hashingAlgoType; + + _AtKeysCryptoImpl(this._hashingAlgoType); + + @override + Future encrypt(String plainAtKeys, String passPhrase, + {HashParams? hashParams}) async { + // 1. Generate hash key based on the hashing algo type: + String hashKey = + await _getHashKey(passPhrase, _hashingAlgoType, hashParams: hashParams); + + AESKey aesKey = AESKey(hashKey); + StringAESEncryptor atEncryptionAlgorithm = StringAESEncryptor(aesKey); + + InitialisationVector iv = AtChopsUtil.generateRandomIV(16); + String encryptedContent = + atEncryptionAlgorithm.encrypt(plainAtKeys, iv: iv); + + return AtEncrypted() + ..content = encryptedContent + ..iv = base64Encode(iv.ivBytes) + ..hashingAlgoType = _hashingAlgoType; + } + + @override + Future decrypt(AtEncrypted atEncrypted, String passPhrase, + {HashParams? hashParams}) async { + if (atEncrypted.iv.isNullOrEmpty) { + throw AtDecryptionException( + 'Initialization vector is required for decryption'); + } + if (atEncrypted.content.isNullOrEmpty) { + throw AtDecryptionException('Cannot decrypt empty or null content'); + } + + // 1. Generate hash key based on the hashing algo type: + String hashKey = + await _getHashKey(passPhrase, _hashingAlgoType, hashParams: hashParams); + AESKey aesKey = AESKey(hashKey); + StringAESEncryptor atEncryptionAlgorithm = StringAESEncryptor(aesKey); + + Uint8List iv = base64Decode(atEncrypted.iv!); + InitialisationVector initialisationVector = InitialisationVector(iv); + + return atEncryptionAlgorithm.decrypt(atEncrypted.content!, + iv: initialisationVector); + } + + /// Generates a hashed key based on the provided [passPhrase] and + /// [hashingAlgoType], with optional [hashParams] for certain algorithms. + /// + /// Returns a [Future] that resolves to a [String] representing the + /// hashed key. + Future _getHashKey(String passPhrase, HashingAlgoType hashingAlgoType, + {HashParams? hashParams}) async { + if (hashingAlgoType == HashingAlgoType.argon2id) { + AtHashingAlgorithm atHashingAlgorithm = Argon2idHashingAlgo(); + return await atHashingAlgorithm.hash(passPhrase, hashParams: hashParams); + } + return DefaultHash().hash(passPhrase.codeUnits); + } +} diff --git a/packages/at_chops/lib/src/model/at_encrypted.dart b/packages/at_chops/lib/src/model/at_encrypted.dart new file mode 100644 index 00000000..52815d04 --- /dev/null +++ b/packages/at_chops/lib/src/model/at_encrypted.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:at_chops/at_chops.dart'; + +/// A class that represents encrypted content, along with metadata such as +/// initialization vector (IV) and the hashing algorithm used. +/// +/// This class is used to serialize and deserialize encrypted data for +/// transmission or storage. It provides methods to convert the object +/// to JSON format and parse it back from JSON. +class AtEncrypted { + /// The encrypted content, typically represented as a Base64 string. + String? content; + + /// The initialization vector (IV) used during encryption, which adds randomness + /// to the encryption process and ensures the same content results in different + /// ciphertexts. + String? iv; + + /// The type of hashing algorithm used for encryption, represented by an + /// enum of type [HashingAlgoType]. + HashingAlgoType? hashingAlgoType; + + /// Converts this [AtEncrypted] instance into a JSON-compatible map. + /// + /// The returned map includes the encrypted content, IV, and the name of the + /// hashing algorithm used. The [hashingAlgoType] is converted to its string name. + /// + /// Returns a [Map] representing the encrypted data. + Map toJson() { + return { + 'content': content, + 'iv': iv, + 'hashingAlgoType': hashingAlgoType?.name + }; + } + + /// Creates an [AtEncrypted] instance from a JSON-compatible map. + /// + /// This method takes a [Map] as input and assigns the corresponding values + /// to the [content], [iv], and [hashingAlgoType] fields. If the hashing + /// algorithm is provided as a string, it is converted back to its enum type + /// using [HashingAlgoType.fromString]. + /// + /// Returns an [AtEncrypted] object populated with data from the map. + static AtEncrypted fromJson(Map map) { + AtEncrypted atEncrypted = AtEncrypted() + ..content = map['content'] + ..iv = map['iv']; + + if (map['hashingAlgoType'] != null && map['hashingAlgoType']!.isNotEmpty) { + atEncrypted.hashingAlgoType = + HashingAlgoType.fromString(map['hashingAlgoType']!); + } + return atEncrypted; + } + + /// Returns the string representation of this [AtEncrypted] instance. + /// + /// This method converts the object into its JSON string form by calling + /// [toJson] and encoding the resulting map using [jsonEncode]. + /// + /// Returns the JSON string representation of the object. + @override + String toString() { + return jsonEncode(toJson()); + } +} diff --git a/packages/at_chops/lib/src/model/hash_params.dart b/packages/at_chops/lib/src/model/hash_params.dart new file mode 100644 index 00000000..70def9a3 --- /dev/null +++ b/packages/at_chops/lib/src/model/hash_params.dart @@ -0,0 +1,36 @@ +/// A class that holds the parameters for configuring a hashing algorithm. +/// +/// This class is used to customize the behavior of a hashing algorithm by +/// providing control over key parameters such as parallelism, memory usage, +/// iteration count, and the length of the resulting hash. +/// +/// These parameters are particularly useful when working with algorithms +/// like Argon2id, which can be adjusted for performance and security needs. +abstract class HashParams {} + +class ArgonHashParams extends HashParams { + /// The degree of parallelism, representing the number of threads used during hashing. + /// + /// The default value is 2, meaning the hashing algorithm will use 2 threads. + int parallelism = 2; + + /// The amount of memory (in KB) to be used during the hashing process. + /// + /// The default value is 10,000 KB (10 MB). Increasing the memory value + /// can make the hashing process more resistant to brute-force attacks. + int memory = 10000; + + /// The number of iterations (time cost) applied during the hashing process. + /// + /// The default value is 2. A higher iteration count increases the time + /// required to compute the hash, providing greater security. + int iterations = 2; + + /// The length of the resulting hash in bytes. + /// + /// The default value is 32 bytes. This value controls the size of the + /// derived hash or key. + int hashLength = 32; +} + +class DefaultHashParams extends HashParams {} diff --git a/packages/at_chops/pubspec.yaml b/packages/at_chops/pubspec.yaml index c9b49231..b1a0d6a2 100644 --- a/packages/at_chops/pubspec.yaml +++ b/packages/at_chops/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: pointycastle: ^3.7.4 at_commons: ^5.0.0 at_utils: ^3.0.19 + cryptography: ^2.7.0 dev_dependencies: lints: ^3.0.0 diff --git a/packages/at_chops/test/at_keys_crypto_test.dart b/packages/at_chops/test/at_keys_crypto_test.dart new file mode 100644 index 00000000..cd9b017c --- /dev/null +++ b/packages/at_chops/test/at_keys_crypto_test.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:at_chops/at_chops.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests to verify atKeys encryption and decryption', () { + test( + 'A test to verify the encryption and decryption of atKeys using a passphrase with the argon2id algorithm', + () async { + String plainAtKeys = jsonEncode({ + 'pkamPublicKey': 'dummy_pkam_public_key', + 'pkamPrivateKey': 'dummy_private_key' + }); + String passPhrase = 'abcd'; + + AtKeysCrypto atKeysCrypto = + AtKeysCrypto.fromHashingAlgorithm(HashingAlgoType.argon2id); + + AtEncrypted atEncrypted = + await atKeysCrypto.encrypt(plainAtKeys, passPhrase); + expect(atEncrypted.content?.isNotEmpty, true); + expect(atEncrypted.iv?.isNotEmpty, true); + expect(atEncrypted.hashingAlgoType, HashingAlgoType.argon2id); + + String decryptedAtKeys = + await atKeysCrypto.decrypt(atEncrypted, passPhrase); + Map dummyAtKeys = jsonDecode(decryptedAtKeys); + expect(dummyAtKeys['pkamPublicKey'], 'dummy_pkam_public_key'); + expect(dummyAtKeys['pkamPrivateKey'], 'dummy_private_key'); + }); + + test('A test to verify the decryption fails when pass phrase is modified', + () async { + String plainAtKeys = jsonEncode({ + 'pkamPublicKey': 'dummy_pkam_public_key', + 'pkamPrivateKey': 'dummy_private_key' + }); + + AtKeysCrypto atKeysCrypto = + AtKeysCrypto.fromHashingAlgorithm(HashingAlgoType.argon2id); + + AtEncrypted atEncrypted = await atKeysCrypto.encrypt(plainAtKeys, 'abcd'); + expect(atEncrypted.content?.isNotEmpty, true); + expect(atEncrypted.iv?.isNotEmpty, true); + expect(atEncrypted.hashingAlgoType, HashingAlgoType.argon2id); + + expect(() async => await atKeysCrypto.decrypt(atEncrypted, 'abcde'), + throwsA(predicate((dynamic e) => e is ArgumentError))); + }); + }); +} diff --git a/packages/at_cli_commons/lib/src/cli_base.dart b/packages/at_cli_commons/lib/src/cli_base.dart index cdd19a64..8baa3beb 100644 --- a/packages/at_cli_commons/lib/src/cli_base.dart +++ b/packages/at_cli_commons/lib/src/cli_base.dart @@ -19,19 +19,19 @@ class CLIBase { ..addFlag('help', negatable: false, help: 'Usage instructions') ..addOption('atsign', abbr: 'a', mandatory: true, help: 'This client\'s atSign') - ..addOption('namespace', abbr: 'n', mandatory: true, help: 'Namespace') - ..addOption('key-file', + ..addOption('namespace', abbr: 'n', mandatory: true, help: 'Namespace')..addOption( + 'key-file', abbr: 'k', mandatory: false, help: 'Your atSign\'s atKeys file if not in ~/.atsign/keys/') ..addOption('cram-secret', abbr: 'c', mandatory: false, help: 'atSign\'s cram secret') - ..addOption('home-dir', abbr: 'h', mandatory: false, help: 'home directory') - ..addOption('storage-dir', + ..addOption('home-dir', abbr: 'h', mandatory: false, help: 'home directory')..addOption( + 'storage-dir', abbr: 's', mandatory: false, - help: 'directory for this client\'s local storage files') - ..addOption('root-domain', + help: 'directory for this client\'s local storage files')..addOption( + 'root-domain', abbr: 'd', mandatory: false, help: 'Root Domain', @@ -92,6 +92,7 @@ class CLIBase { final String? storageDir; final String? downloadDir; final String? cramSecret; + final String? passPhrase; final bool syncDisabled; final int maxConnectAttempts; @@ -120,19 +121,19 @@ class CLIBase { /// cliBase.logger.logger.level = Level.FINEST; /// ``` /// Throws an [IllegalArgumentException] if the parameters fail validation. - CLIBase({ - required String atSign, - required this.nameSpace, - required this.rootDomain, - this.homeDir, - this.verbose = false, - this.atKeysFilePath, - this.storageDir, - this.downloadDir, - this.cramSecret, - this.syncDisabled = false, - this.maxConnectAttempts = defaultMaxConnectAttempts, - }) { + CLIBase( + {required String atSign, + required this.nameSpace, + required this.rootDomain, + this.homeDir, + this.verbose = false, + this.atKeysFilePath, + this.storageDir, + this.downloadDir, + this.cramSecret, + this.syncDisabled = false, + this.maxConnectAttempts = defaultMaxConnectAttempts, + this.passPhrase}) { this.atSign = AtUtils.fixAtSign(atSign); if (homeDir == null) { if (atKeysFilePath == null) { @@ -196,7 +197,8 @@ class CLIBase { ..fetchOfflineNotifications = true ..atKeysFilePath = atKeysFilePathToUse ..cramSecret = cramSecret - ..atProtocolEmitted = Version(2, 0, 0); + ..atProtocolEmitted = Version(2, 0, 0) + ..passPhrase = passPhrase; AtOnboardingService onboardingService = AtOnboardingServiceImpl( atSign, atOnboardingConfig, diff --git a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart index ba62e6bf..b9ae0232 100644 --- a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart +++ b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:at_auth/at_auth.dart'; +import 'package:at_chops/at_chops.dart'; import 'package:at_cli_commons/at_cli_commons.dart'; import 'package:at_client/at_client.dart'; import 'package:at_commons/at_builders.dart'; @@ -290,17 +291,17 @@ Future createAtClient(ArgResults ar) async { ); CLIBase cliBase = CLIBase( - atSign: atSign, - atKeysFilePath: ar[AuthCliArgs.argNameAtKeys], - nameSpace: nameSpace, - rootDomain: ar[AuthCliArgs.argNameAtDirectoryFqdn], - homeDir: getHomeDirectory(), - storageDir: storageDir!.path, - verbose: ar[AuthCliArgs.argNameVerbose] || ar[AuthCliArgs.argNameDebug], - syncDisabled: true, - maxConnectAttempts: int.parse( - ar[AuthCliArgs.argNameMaxConnectAttempts]), // 10 * 3 == 30 seconds - ); + atSign: atSign, + atKeysFilePath: ar[AuthCliArgs.argNameAtKeys], + nameSpace: nameSpace, + rootDomain: ar[AuthCliArgs.argNameAtDirectoryFqdn], + homeDir: getHomeDirectory(), + storageDir: storageDir!.path, + verbose: ar[AuthCliArgs.argNameVerbose] || ar[AuthCliArgs.argNameDebug], + syncDisabled: true, + maxConnectAttempts: int.parse(ar[AuthCliArgs.argNameMaxConnectAttempts]), + // 10 * 3 == 30 seconds + passPhrase: ar[AuthCliArgs.argNamePassPhrase]); await cliBase.init(); @@ -996,7 +997,10 @@ AtOnboardingService createOnboardingService(ArgResults ar) { ..rootDomain = ar[AuthCliArgs.argNameAtDirectoryFqdn] ..registrarUrl = ar[AuthCliArgs.argNameRegistrarFqdn] ..cramSecret = ar[AuthCliArgs.argNameCramSecret] - ..atKeysFilePath = ar[AuthCliArgs.argNameAtKeys]; + ..atKeysFilePath = ar[AuthCliArgs.argNameAtKeys] + ..passPhrase = ar[AuthCliArgs.argNamePassPhrase] + ..hashingAlgoType = + HashingAlgoType.fromString(ar[AuthCliArgs.argNameHashingAlgoType]); return AtOnboardingServiceImpl(atSign, atOnboardingPreference); } diff --git a/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart b/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart index 445dff7c..99491168 100644 --- a/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart +++ b/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:args/args.dart'; +import 'package:at_chops/at_chops.dart'; import 'package:at_commons/at_commons.dart'; import 'package:meta/meta.dart'; @@ -110,6 +111,8 @@ class AuthCliArgs { static const argNameExpiry = 'expiry'; static const argAbbrExpiry = 'e'; static const argNameAutoApproveExisting = 'approve-existing'; + static const argNamePassPhrase = 'passPhrase'; + static const argNameHashingAlgoType = 'hashingAlgoType'; ArgParser get parser { return _aap; @@ -267,7 +270,17 @@ class AuthCliArgs { mandatory: false, hide: !forOnboard, ); - + p.addOption(argNamePassPhrase, + abbr: 'P', + help: + 'Pass Phrase to encrypt/decrypt the password protected atKeys file', + mandatory: false, + hide: hide); + p.addOption(argNameHashingAlgoType, + help: 'Hashing algorithm type. Defaults to argon2id', + mandatory: false, + defaultsTo: HashingAlgoType.argon2id.name, + hide: hide); return p; } diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 3db31f3a..2db825fd 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -428,9 +428,18 @@ class AtOnboardingServiceImpl implements AtOnboardingService { atKeysFile.createSync(recursive: true); IOSink fileWriter = atKeysFile.openWrite(); + String encodedAtKeysString = jsonEncode(atKeysMap); + if (atOnboardingPreference.passPhrase != null) { + AtEncrypted atEncrypted = await AtKeysCrypto.fromHashingAlgorithm( + atOnboardingPreference.hashingAlgoType) + .encrypt(encodedAtKeysString, atOnboardingPreference.passPhrase!); + encodedAtKeysString = atEncrypted.toString(); + stdout.writeln( + '[Information] Encrypted atKeys file with the given pass phrase'); + } //generating .atKeys file at path provided in onboardingConfig - fileWriter.write(jsonEncode(atKeysMap)); + fileWriter.write(encodedAtKeysString); await fileWriter.flush(); await fileWriter.close(); stdout.writeln( @@ -441,10 +450,7 @@ class AtOnboardingServiceImpl implements AtOnboardingService { ///back-up encryption keys to local secondary /// #TODO remove this method in future when all keys are read from AtChops - Future _persistKeysLocalSecondary() async { - //when authenticating keys need to be fetched from atKeys file - at_auth.AtAuthKeys atAuthKeys = _decryptAtKeysFile( - (await readAtKeysFile(atOnboardingPreference.atKeysFilePath))); + Future _persistKeysLocalSecondary(at_auth.AtAuthKeys atAuthKeys) async { //backup keys into local secondary bool? response = await atClient ?.getLocalSecondary() @@ -481,7 +487,8 @@ class AtOnboardingServiceImpl implements AtOnboardingService { ..authMode = atOnboardingPreference.authMode ..rootDomain = atOnboardingPreference.rootDomain ..rootPort = atOnboardingPreference.rootPort - ..publicKeyId = atOnboardingPreference.publicKeyId; + ..publicKeyId = atOnboardingPreference.publicKeyId + ..passPhrase = atOnboardingPreference.passPhrase; var atAuthResponse = await atAuth!.authenticate(atAuthRequest); logger.finer('Auth response: $atAuthResponse'); if (atAuthResponse.isSuccessful && @@ -489,7 +496,7 @@ class AtOnboardingServiceImpl implements AtOnboardingService { logger.finer('Calling persist keys to local secondary'); await _initAtClient(atAuth!.atChops!, enrollmentId: atAuthResponse.enrollmentId); - await _persistKeysLocalSecondary(); + await _persistKeysLocalSecondary(atAuthResponse.atAuthKeys!); } return atAuthResponse.isSuccessful; @@ -511,33 +518,6 @@ class AtOnboardingServiceImpl implements AtOnboardingService { return jsonData; } - ///method to extract decryption key from atKeysData - ///returns self_encryption_key - String _getDecryptionKey(Map? jsonData) { - return jsonData![AuthKeyType.selfEncryptionKey]!; - } - - at_auth.AtAuthKeys _decryptAtKeysFile(Map jsonData) { - var atAuthKeys = at_auth.AtAuthKeys(); - String decryptionKey = _getDecryptionKey(jsonData); - atAuthKeys.defaultEncryptionPublicKey = EncryptionUtil.decryptValue( - jsonData[AuthKeyType.encryptionPublicKey]!, decryptionKey); - atAuthKeys.defaultEncryptionPrivateKey = EncryptionUtil.decryptValue( - jsonData[AuthKeyType.encryptionPrivateKey]!, decryptionKey); - atAuthKeys.defaultSelfEncryptionKey = decryptionKey; - atAuthKeys.apkamPublicKey = EncryptionUtil.decryptValue( - jsonData[AuthKeyType.pkamPublicKey]!, decryptionKey); - // pkam private key will not be saved in keyfile if auth mode is sim/any other secure element. - // decrypt the private key only when auth mode is keysFile - if (atOnboardingPreference.authMode == PkamAuthMode.keysFile) { - atAuthKeys.apkamPrivateKey = EncryptionUtil.decryptValue( - jsonData[AuthKeyType.pkamPrivateKey]!, decryptionKey); - } - atAuthKeys.apkamSymmetricKey = jsonData[AuthKeyType.apkamSymmetricKey]; - atAuthKeys.enrollmentId = jsonData[AtConstants.enrollmentId]; - return atAuthKeys; - } - ///generates random RSA keypair RSAKeypair generateRsaKeypair() { return RSAKeypair.fromRandom(); diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index 54b555db..ca7c15ac 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -27,4 +27,6 @@ class AtOnboardingPreference extends AtClientPreference { @Deprecated("No longer used") int apkamAuthRetryDurationMins = 30; + + String? passPhrase; } diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index bfd75ae3..a7295a5f 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -31,6 +31,13 @@ dependencies: at_cli_commons: ^1.2.0 at_persistence_secondary_server: ^3.0.64 duration: ^4.0.3 + crypto: ^3.0.5 + +dependency_overrides: + at_auth: + path: ../at_auth + at_cli_commons: + path: ../at_cli_commons dev_dependencies: lints: ^2.1.0 diff --git a/packages/at_onboarding_cli/test/at_onboarding_cli_test.dart b/packages/at_onboarding_cli/test/at_onboarding_cli_test.dart index 3fc3d73e..31cc893b 100644 --- a/packages/at_onboarding_cli/test/at_onboarding_cli_test.dart +++ b/packages/at_onboarding_cli/test/at_onboarding_cli_test.dart @@ -56,8 +56,17 @@ void main() { atSign, '.wavi', getAtClientPreferenceAlice()); when(() => mockAtLookup.pkamAuthenticate()) .thenAnswer((_) => Future.value(true)); - when(() => mockAtAuth.authenticate(any())).thenAnswer( - (_) => Future.value(AtAuthResponse(atSign)..isSuccessful = true)); + when(() => mockAtAuth.authenticate(any())) + .thenAnswer((_) => Future.value(AtAuthResponse(atSign) + ..isSuccessful = true + ..atAuthKeys = (AtAuthKeys() + ..apkamPublicKey = 'dummy_apkam_public_key' + ..apkamPrivateKey = 'dummy_private_key' + ..defaultSelfEncryptionKey = 'dummy_self_encryption_key' + ..defaultEncryptionPrivateKey = 'dummy_enc_priv_key' + ..defaultEncryptionPublicKey = 'dummy_enc_pub_key' + ..apkamSymmetricKey = 'dummy_apkam_sym_key' + ..enrollmentId = 'dummy_enroll_id'))); when(() => mockAtAuth.atChops) .thenAnswer((_) => AtChopsImpl(AtChopsKeys())); var authResult = await onboardingService.authenticate(); diff --git a/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart b/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart index 5efe95eb..a82f8a6f 100644 --- a/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart +++ b/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:at_auth/at_auth.dart'; +import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; import 'package:at_onboarding_cli/src/cli/auth_cli.dart' as auth_cli; @@ -11,6 +12,34 @@ void main() { String atSign = '@sitaramđź› '; String apkamKeysFilePath = 'storage/keys/@sitaram-apkam.atKeys'; final logger = AtSignLogger('E2E Test'); + late AtOnboardingService atOnboardingService; + + // Runs once before all tests. + setUpAll(() async { + atOnboardingService = AtOnboardingServiceImpl( + atSign, + getOnboardingPreference(atSign, + '${Platform.environment['HOME']}/.atsign/keys/${atSign}_key.atKeys') + // Fetched cram key from the at_demos repo. + ..cramSecret = + '15cdce8f92bcf7e742d5b75dc51ec06d798952f8bf7e8ff4c2b6448e5f7c2c12b570fe945f04011455fdc49cacdf9393d9c1ac4609ec71c1a0b0c213578e7ec7'); + + bool onboardingStatus = await atOnboardingService.onboard(); + expect(onboardingStatus, true); + // Set SPP + List args = [ + 'spp', + '-s', + 'ABC123', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone' + ]; + var res = await auth_cli.wrappedMain(args); + // Zero indicates successful completion. + expect(res, 0); + }); group('A group of tests to validate enrollment commands', () { /// The test verifies the following scenario's @@ -26,30 +55,6 @@ void main() { test( 'A test to verify end-to-end flow of approve revoke unrevoke of an enrollment', () async { - AtOnboardingService atOnboardingService = AtOnboardingServiceImpl( - atSign, - getOnboardingPreference(atSign, - '${Platform.environment['HOME']}/.atsign/keys/${atSign}_key.atKeys') - // Fetched cram key from the at_demos repo. - ..cramSecret = - '15cdce8f92bcf7e742d5b75dc51ec06d798952f8bf7e8ff4c2b6448e5f7c2c12b570fe945f04011455fdc49cacdf9393d9c1ac4609ec71c1a0b0c213578e7ec7'); - - bool onboardingStatus = await atOnboardingService.onboard(); - expect(onboardingStatus, true); - // Set SPP - List args = [ - 'spp', - '-s', - 'ABC123', - '-a', - atSign, - '-r', - 'vip.ve.atsign.zone' - ]; - var res = await auth_cli.wrappedMain(args); - // Zero indicates successful completion. - expect(res, 0); - // Submit enrollment request AtEnrollmentResponse atEnrollmentResponse = await atOnboardingService .sendEnrollRequest( @@ -60,7 +65,7 @@ void main() { expect(atEnrollmentResponse.enrollmentId.isNotEmpty, true); // Approve enrollment request - args = [ + List args = [ 'approve', '-a', atSign, @@ -69,7 +74,7 @@ void main() { '-i', atEnrollmentResponse.enrollmentId ]; - res = await auth_cli.wrappedMain(args); + var res = await auth_cli.wrappedMain(args); expect(res, 0); logger.info( 'Approved enrollment with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); @@ -125,11 +130,75 @@ void main() { enrollmentId: atEnrollmentResponse.enrollmentId); expect(authResponse, true); }); + + test('A test to verify password protected of atKeys file', () async { + // Set pass-phrase to encrypt the atKeys file upon approval of enrollment request. + (atOnboardingService as AtOnboardingServiceImpl) + .atOnboardingPreference + .passPhrase = 'abcd'; + (atOnboardingService as AtOnboardingServiceImpl) + .atOnboardingPreference + .hashingAlgoType = HashingAlgoType.argon2id; + // Submit enrollment request + AtEnrollmentResponse atEnrollmentResponse = await atOnboardingService + .sendEnrollRequest( + 'buzz', 'local-device', 'ABC123', {'e2etest': 'rw'}); + logger.info( + 'Submitted enrollment successfully with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); + expect(atEnrollmentResponse.enrollStatus, EnrollmentStatus.pending); + expect(atEnrollmentResponse.enrollmentId.isNotEmpty, true); + // Approve enrollment request + List args = [ + 'approve', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone', + '-i', + atEnrollmentResponse.enrollmentId + ]; + var res = await auth_cli.wrappedMain(args); + expect(res, 0); + logger.info( + 'Approved enrollment with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); + + // Generate Atkeys file for the enrollment request. + await atOnboardingService.awaitApproval(atEnrollmentResponse); + await atOnboardingService.createAtKeysFile(atEnrollmentResponse, + atKeysFile: + File('storage/keys/@sitaram-apkam-password-protected.atKeys')); + + // Authenticate with APKAM keys + (atOnboardingService as AtOnboardingServiceImpl) + .atOnboardingPreference + .atKeysFilePath = + 'storage/keys/@sitaram-apkam-password-protected.atKeys'; + bool authResponse = await atOnboardingService.authenticate( + enrollmentId: atEnrollmentResponse.enrollmentId); + expect(authResponse, true); + + // Run list to ensure the pass-phase is indeed working as expected + args = [ + 'list', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone', + '-P', + (atOnboardingService as AtOnboardingServiceImpl) + .atOnboardingPreference + .passPhrase!, + '-k', + 'storage/keys/@sitaram-apkam-password-protected.atKeys' + ]; + res = await auth_cli.wrappedMain(args); + // Zero indicate successful completion. + expect(res, 0); + }); }); - tearDown(() { - File file = File(apkamKeysFilePath); - file.deleteSync(); + tearDownAll(() { + Directory('storage').deleteSync(recursive: true); }); } From 2fb5481018409289598d6119fb7c11615da10e4a Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Wed, 30 Oct 2024 16:22:56 +0530 Subject: [PATCH 2/6] fix: Add dependency overrides for build to pass --- packages/at_cli_commons/pubspec.yaml | 4 ++++ packages/at_onboarding_cli/pubspec.yaml | 2 ++ tests/at_onboarding_cli_functional_tests/pubspec.yaml | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/packages/at_cli_commons/pubspec.yaml b/packages/at_cli_commons/pubspec.yaml index e5342dc3..b84c6d78 100644 --- a/packages/at_cli_commons/pubspec.yaml +++ b/packages/at_cli_commons/pubspec.yaml @@ -19,6 +19,10 @@ dependencies: meta: ^1.11.0 path: ^1.9.0 +dependency_overrides: + at_onboarding_cli: + path: ../at_onboarding_cli + dev_dependencies: lints: ^3.0.0 test: ^1.24.9 diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index a7295a5f..16550b4b 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -38,6 +38,8 @@ dependency_overrides: path: ../at_auth at_cli_commons: path: ../at_cli_commons + at_chops: + path: ../at_chops dev_dependencies: lints: ^2.1.0 diff --git a/tests/at_onboarding_cli_functional_tests/pubspec.yaml b/tests/at_onboarding_cli_functional_tests/pubspec.yaml index ed30a139..6b061fcd 100644 --- a/tests/at_onboarding_cli_functional_tests/pubspec.yaml +++ b/tests/at_onboarding_cli_functional_tests/pubspec.yaml @@ -18,6 +18,10 @@ dependency_overrides: path: ../../packages/at_onboarding_cli at_commons: path: ../../packages/at_commons + at_chops: + path: ../../packages/at_chops + at_cli_commons: + path: ../../packages/at_cli_commons dev_dependencies: lints: ^1.0.0 From de9bfee70dda7844940c6d264f3538b4e8af334e Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Wed, 30 Oct 2024 16:26:31 +0530 Subject: [PATCH 3/6] fix: Add dependency overrides to at_cli_commons --- packages/at_cli_commons/pubspec.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/at_cli_commons/pubspec.yaml b/packages/at_cli_commons/pubspec.yaml index b84c6d78..ea6cf9db 100644 --- a/packages/at_cli_commons/pubspec.yaml +++ b/packages/at_cli_commons/pubspec.yaml @@ -22,6 +22,10 @@ dependencies: dependency_overrides: at_onboarding_cli: path: ../at_onboarding_cli + at_auth: + path: ../at_auth + at_chops: + path: ../at_chops dev_dependencies: lints: ^3.0.0 From d3b8dbe68eac63ded6c28c84b06b6e7a6dbbae27 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Mon, 4 Nov 2024 07:32:54 +0530 Subject: [PATCH 4/6] fix: Add pass-phrase to cli arg parser and run dart formatter --- packages/at_auth/lib/src/at_auth_impl.dart | 2 +- packages/at_cli_commons/lib/src/cli_base.dart | 44 +++++++++++-------- .../src/util/at_onboarding_preference.dart | 1 + 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/at_auth/lib/src/at_auth_impl.dart b/packages/at_auth/lib/src/at_auth_impl.dart index 9bfb087d..71145115 100644 --- a/packages/at_auth/lib/src/at_auth_impl.dart +++ b/packages/at_auth/lib/src/at_auth_impl.dart @@ -312,7 +312,7 @@ class AtAuthImpl implements AtAuth { if (atEncrypted.hashingAlgoType == null) { throw AtDecryptionException( - 'Hashing algo type is required for decryption of AtKeys file'); + 'Hashing algo type is required for decryption of password protected atKeys file'); } String decryptedAtKeys = diff --git a/packages/at_cli_commons/lib/src/cli_base.dart b/packages/at_cli_commons/lib/src/cli_base.dart index 8baa3beb..df816bf3 100644 --- a/packages/at_cli_commons/lib/src/cli_base.dart +++ b/packages/at_cli_commons/lib/src/cli_base.dart @@ -18,20 +18,21 @@ class CLIBase { static final ArgParser argsParser = ArgParser() ..addFlag('help', negatable: false, help: 'Usage instructions') ..addOption('atsign', - abbr: 'a', mandatory: true, help: 'This client\'s atSign') - ..addOption('namespace', abbr: 'n', mandatory: true, help: 'Namespace')..addOption( + abbr: 'a', mandatory: true, help: 'This client\'s atSign')..addOption( + 'namespace', abbr: 'n', mandatory: true, help: 'Namespace')..addOption( 'key-file', abbr: 'k', mandatory: false, help: 'Your atSign\'s atKeys file if not in ~/.atsign/keys/') ..addOption('cram-secret', - abbr: 'c', mandatory: false, help: 'atSign\'s cram secret') - ..addOption('home-dir', abbr: 'h', mandatory: false, help: 'home directory')..addOption( - 'storage-dir', + abbr: 'c', mandatory: false, help: 'atSign\'s cram secret')..addOption( + 'home-dir', abbr: 'h', + mandatory: false, + help: 'home directory')..addOption('storage-dir', abbr: 's', mandatory: false, - help: 'directory for this client\'s local storage files')..addOption( - 'root-domain', + help: 'directory for this client\'s local storage files') + ..addOption('root-domain', abbr: 'd', mandatory: false, help: 'Root Domain', @@ -41,7 +42,12 @@ class CLIBase { ..addOption('max-connect-attempts', help: 'Number of times to attempt to initially connect to atServer.' ' Note: there is a 3-second delay between connection attempts.', - defaultsTo: defaultMaxConnectAttempts.toString()); + defaultsTo: defaultMaxConnectAttempts.toString()) + ..addOption('passPhrase', + abbr: 'P', + help: + 'Pass Phrase to encrypt/decrypt the password protected atKeys file', + mandatory: false); /// Constructs a CLIBase from a list of command-line arguments /// and calls [init] on it. @@ -67,17 +73,17 @@ class CLIBase { } CLIBase cliBase = CLIBase( - atSign: parsedArgs['atsign'], - atKeysFilePath: parsedArgs['key-file'], - nameSpace: parsedArgs['namespace'], - rootDomain: parsedArgs['root-domain'], - homeDir: getHomeDirectory(), - storageDir: parsedArgs['storage-dir'], - verbose: parsedArgs['verbose'], - cramSecret: parsedArgs['cram-secret'], - syncDisabled: parsedArgs['never-sync'], - maxConnectAttempts: int.parse(parsedArgs['max-connect-attempts']), - ); + atSign: parsedArgs['atsign'], + atKeysFilePath: parsedArgs['key-file'], + nameSpace: parsedArgs['namespace'], + rootDomain: parsedArgs['root-domain'], + homeDir: getHomeDirectory(), + storageDir: parsedArgs['storage-dir'], + verbose: parsedArgs['verbose'], + cramSecret: parsedArgs['cram-secret'], + syncDisabled: parsedArgs['never-sync'], + maxConnectAttempts: int.parse(parsedArgs['max-connect-attempts']), + passPhrase: parsedArgs['passPhrase']); await cliBase.init(); diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index ca7c15ac..4848e5ec 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -28,5 +28,6 @@ class AtOnboardingPreference extends AtClientPreference { @Deprecated("No longer used") int apkamAuthRetryDurationMins = 30; + /// The password (or pass-phrase) with which the atKeys file is encrypted/decrypted. String? passPhrase; } From 6877c41a8eec3e7a78001d5d4d7d537f13f48cf8 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Wed, 6 Nov 2024 14:19:16 +0530 Subject: [PATCH 5/6] fix: Introduce AtHashingAlgorithmFactory and minor code changes --- .../at_chops/lib/src/algorithm/algo_type.dart | 15 ++------- .../algorithm/at_hashing_algo_factory.dart | 31 +++++++++++++++++++ packages/at_chops/lib/src/at_keys_crypto.dart | 11 +++---- packages/at_cli_commons/lib/src/cli_base.dart | 6 ++-- 4 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart diff --git a/packages/at_chops/lib/src/algorithm/algo_type.dart b/packages/at_chops/lib/src/algorithm/algo_type.dart index 452e4faf..2e109180 100644 --- a/packages/at_chops/lib/src/algorithm/algo_type.dart +++ b/packages/at_chops/lib/src/algorithm/algo_type.dart @@ -10,17 +10,8 @@ enum HashingAlgoType { argon2id; static HashingAlgoType fromString(String name) { - switch (name.toLowerCase()) { - case 'sha256': - return HashingAlgoType.sha256; - case 'sha512': - return HashingAlgoType.sha512; - case 'md5': - return HashingAlgoType.md5; - case 'argon2id': - return HashingAlgoType.argon2id; - default: - throw AtException('Invalid hashing algo type'); - } + return HashingAlgoType.values.firstWhere( + (algo) => algo.name == name.toLowerCase(), + orElse: () => throw AtException('Invalid hashing algo type')); } } diff --git a/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart b/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart new file mode 100644 index 00000000..2b7fae67 --- /dev/null +++ b/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart @@ -0,0 +1,31 @@ +import 'package:at_chops/src/algorithm/algo_type.dart'; +import 'package:at_chops/src/algorithm/argon2id_hashing_algo.dart'; +import 'package:at_chops/src/algorithm/at_algorithm.dart'; +import 'package:at_chops/src/algorithm/default_hashing_algo.dart'; +import 'package:at_commons/at_commons.dart'; + +/// A factory class for creating instances of different hashing algorithms +/// based on the specified [HashingAlgoType]. +/// +/// The [AtHashingAlgorithmFactory] class provides a static method +/// [getHashingAlgorithm] which returns the appropriate hashing algorithm +/// implementation corresponding to the provided [HashingAlgoType]. +class AtHashingAlgorithmFactory { + /// Returns an instance of [AtHashingAlgorithm] based on the provided [HashingAlgoType]. + /// + /// The method supports the following hashing algorithms: + /// - [HashingAlgoType.md5]: returns an instance of [DefaultHash] (MD5 hashing). + /// - [HashingAlgoType.argon2id]: returns an instance of [Argon2idHashingAlgo] (Argon2id hashing). + /// + /// Throws an [AtException] if an unsupported hashing algorithm is passed. + static AtHashingAlgorithm getHashingAlgorithm(HashingAlgoType algoType) { + switch (algoType) { + case HashingAlgoType.md5: + return DefaultHash(); + case HashingAlgoType.argon2id: + return Argon2idHashingAlgo(); + default: + throw AtException('Unsupported hashing algorithm'); + } + } +} diff --git a/packages/at_chops/lib/src/at_keys_crypto.dart b/packages/at_chops/lib/src/at_keys_crypto.dart index 39b6fa05..fe2764a6 100644 --- a/packages/at_chops/lib/src/at_keys_crypto.dart +++ b/packages/at_chops/lib/src/at_keys_crypto.dart @@ -3,9 +3,8 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:at_chops/at_chops.dart'; -import 'package:at_chops/src/algorithm/argon2id_hashing_algo.dart'; import 'package:at_chops/src/algorithm/at_algorithm.dart'; -import 'package:at_chops/src/algorithm/default_hashing_algo.dart'; +import 'package:at_chops/src/algorithm/at_hashing_algo_factory.dart'; import 'package:at_chops/src/model/hash_params.dart'; import 'package:at_commons/at_commons.dart'; @@ -111,10 +110,8 @@ class _AtKeysCryptoImpl implements AtKeysCrypto { /// hashed key. Future _getHashKey(String passPhrase, HashingAlgoType hashingAlgoType, {HashParams? hashParams}) async { - if (hashingAlgoType == HashingAlgoType.argon2id) { - AtHashingAlgorithm atHashingAlgorithm = Argon2idHashingAlgo(); - return await atHashingAlgorithm.hash(passPhrase, hashParams: hashParams); - } - return DefaultHash().hash(passPhrase.codeUnits); + AtHashingAlgorithm atHashingAlgorithm = + AtHashingAlgorithmFactory.getHashingAlgorithm(hashingAlgoType); + return await atHashingAlgorithm.hash(passPhrase, hashParams: hashParams); } } diff --git a/packages/at_cli_commons/lib/src/cli_base.dart b/packages/at_cli_commons/lib/src/cli_base.dart index df816bf3..2373d8db 100644 --- a/packages/at_cli_commons/lib/src/cli_base.dart +++ b/packages/at_cli_commons/lib/src/cli_base.dart @@ -42,8 +42,8 @@ class CLIBase { ..addOption('max-connect-attempts', help: 'Number of times to attempt to initially connect to atServer.' ' Note: there is a 3-second delay between connection attempts.', - defaultsTo: defaultMaxConnectAttempts.toString()) - ..addOption('passPhrase', + defaultsTo: defaultMaxConnectAttempts.toString())..addOption( + 'pass-phrase', abbr: 'P', help: 'Pass Phrase to encrypt/decrypt the password protected atKeys file', @@ -83,7 +83,7 @@ class CLIBase { cramSecret: parsedArgs['cram-secret'], syncDisabled: parsedArgs['never-sync'], maxConnectAttempts: int.parse(parsedArgs['max-connect-attempts']), - passPhrase: parsedArgs['passPhrase']); + passPhrase: parsedArgs['pass-phrase']); await cliBase.init(); From 6f3f785a53cc638c62fdecd18ab7b4c21e5d687e Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Fri, 8 Nov 2024 07:11:25 +0530 Subject: [PATCH 6/6] fix: Remove DefaultHash from the at_hashing_algo_factory.dart --- .../at_chops/lib/src/algorithm/at_hashing_algo_factory.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart b/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart index 2b7fae67..6aa2f7da 100644 --- a/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart +++ b/packages/at_chops/lib/src/algorithm/at_hashing_algo_factory.dart @@ -20,8 +20,6 @@ class AtHashingAlgorithmFactory { /// Throws an [AtException] if an unsupported hashing algorithm is passed. static AtHashingAlgorithm getHashingAlgorithm(HashingAlgoType algoType) { switch (algoType) { - case HashingAlgoType.md5: - return DefaultHash(); case HashingAlgoType.argon2id: return Argon2idHashingAlgo(); default: