From 2f844f09712d6a8b47dc27a0c6f39fa321044300 Mon Sep 17 00:00:00 2001 From: murali-shris Date: Fri, 13 Oct 2023 12:08:32 +0530 Subject: [PATCH 1/6] feat: at_auth package initial commit --- packages/at_auth/.gitignore | 10 + packages/at_auth/CHANGELOG.md | 2 + packages/at_auth/README.md | 33 ++ packages/at_auth/analysis_options.yaml | 30 ++ packages/at_auth/example/authenticate.dart | 12 + packages/at_auth/example/onboard_apkam.dart | 15 + packages/at_auth/example/onboard_legacy.dart | 12 + packages/at_auth/lib/at_auth.dart | 10 + packages/at_auth/lib/src/at_auth_base.dart | 25 ++ packages/at_auth/lib/src/at_auth_impl.dart | 341 ++++++++++++++++++ .../at_auth/lib/src/auth/at_auth_request.dart | 16 + .../lib/src/auth/at_auth_response.dart | 11 + .../lib/src/auth/cram_authenticator.dart | 24 ++ .../lib/src/auth/pkam_authenticator.dart | 22 ++ packages/at_auth/lib/src/auth_constants.dart | 7 + .../lib/src/exception/at_auth_exceptions.dart | 10 + .../at_auth/lib/src/keys/at_auth_keys.dart | 28 ++ .../src/onboard/at_onboarding_request.dart | 15 + .../src/onboard/at_onboarding_response.dart | 14 + packages/at_auth/pubspec.yaml | 22 ++ packages/at_auth/test/at_auth_test.dart | 259 +++++++++++++ .../at_auth/test/cram_authenticator_test.dart | 41 +++ .../data/@alice\360\237\233\240_key.atKeys" | 1 + .../at_auth/test/pkam_authenticator_test.dart | 44 +++ 24 files changed, 1004 insertions(+) create mode 100644 packages/at_auth/.gitignore create mode 100644 packages/at_auth/CHANGELOG.md create mode 100644 packages/at_auth/README.md create mode 100644 packages/at_auth/analysis_options.yaml create mode 100644 packages/at_auth/example/authenticate.dart create mode 100644 packages/at_auth/example/onboard_apkam.dart create mode 100644 packages/at_auth/example/onboard_legacy.dart create mode 100644 packages/at_auth/lib/at_auth.dart create mode 100644 packages/at_auth/lib/src/at_auth_base.dart create mode 100644 packages/at_auth/lib/src/at_auth_impl.dart create mode 100644 packages/at_auth/lib/src/auth/at_auth_request.dart create mode 100644 packages/at_auth/lib/src/auth/at_auth_response.dart create mode 100644 packages/at_auth/lib/src/auth/cram_authenticator.dart create mode 100644 packages/at_auth/lib/src/auth/pkam_authenticator.dart create mode 100644 packages/at_auth/lib/src/auth_constants.dart create mode 100644 packages/at_auth/lib/src/exception/at_auth_exceptions.dart create mode 100644 packages/at_auth/lib/src/keys/at_auth_keys.dart create mode 100644 packages/at_auth/lib/src/onboard/at_onboarding_request.dart create mode 100644 packages/at_auth/lib/src/onboard/at_onboarding_response.dart create mode 100644 packages/at_auth/pubspec.yaml create mode 100644 packages/at_auth/test/at_auth_test.dart create mode 100644 packages/at_auth/test/cram_authenticator_test.dart create mode 100644 "packages/at_auth/test/data/@alice\360\237\233\240_key.atKeys" create mode 100644 packages/at_auth/test/pkam_authenticator_test.dart diff --git a/packages/at_auth/.gitignore b/packages/at_auth/.gitignore new file mode 100644 index 00000000..b7290373 --- /dev/null +++ b/packages/at_auth/.gitignore @@ -0,0 +1,10 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ +.packages + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +at_auth.iml diff --git a/packages/at_auth/CHANGELOG.md b/packages/at_auth/CHANGELOG.md new file mode 100644 index 00000000..3c9133cc --- /dev/null +++ b/packages/at_auth/CHANGELOG.md @@ -0,0 +1,2 @@ +## 1.0.0 +- Implemented onboard and authenticate methods. diff --git a/packages/at_auth/README.md b/packages/at_auth/README.md new file mode 100644 index 00000000..d1345736 --- /dev/null +++ b/packages/at_auth/README.md @@ -0,0 +1,33 @@ +Package for onboarding and authentication to an atsign's secondary server + +## Features + +- onboard logic - cram authentication,pkam/encryption/apkam key pair generation, initial pkam authentication +- authentication - read keys from .atKeys file, pkam authentication + +## Getting started + +- Developers should have a free/paid atsign from https://atsign.com/ + +## Usage + +Onboard an atsign +```dart +final atAuth = AtAuthImpl(); +final atOnboardingRequest = AtOnboardingRequest('@alice') + ..rootDomain = 'vip.ve.atsign.zone' + ..enableEnrollment = true + ..appName = 'wavi' + ..deviceName = 'iphone'; +final atOnboardingResponse = await atAuth.onboard(atOnboardingRequest, ); +``` + +Authenticate an atsign +```dart +final atAuth = AtAuthImpl(); +final atAuthRequest = AtAuthRequest('@alice') + ..rootDomain = 'vip.ve.atsign.zone' + ..atKeysFilePath = args[1]; +final atAuthResponse = await atAuth.authenticate(atAuthRequest); +``` + diff --git a/packages/at_auth/analysis_options.yaml b/packages/at_auth/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/packages/at_auth/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/at_auth/example/authenticate.dart b/packages/at_auth/example/authenticate.dart new file mode 100644 index 00000000..559ceb1e --- /dev/null +++ b/packages/at_auth/example/authenticate.dart @@ -0,0 +1,12 @@ +import 'package:at_auth/at_auth.dart'; + +/// dart authenticate.dart +void main(List args) async { + final atAuth = AtAuthImpl(); + final atSign = args[0]; + final atAuthRequest = AtAuthRequest(atSign) + ..rootDomain = 'vip.ve.atsign.zone' + ..atKeysFilePath = args[1]; + final atAuthResponse = await atAuth.authenticate(atAuthRequest); + print('atAuthResponse: $atAuthResponse'); +} diff --git a/packages/at_auth/example/onboard_apkam.dart b/packages/at_auth/example/onboard_apkam.dart new file mode 100644 index 00000000..6fb46717 --- /dev/null +++ b/packages/at_auth/example/onboard_apkam.dart @@ -0,0 +1,15 @@ +import 'package:at_auth/at_auth.dart'; + +/// dart onboard_apkam.dart +void main(List args) async { + final atAuth = AtAuthImpl(); + final atSign = args[0]; + final atOnboardingRequest = AtOnboardingRequest(atSign) + ..rootDomain = 'vip.ve.atsign.zone' + ..enableEnrollment = true + ..appName = 'wavi' + ..deviceName = 'iphone'; + final atOnboardingResponse = + await atAuth.onboard(atOnboardingRequest, args[1]); + print('atOnboardingResponse: $atOnboardingResponse'); +} diff --git a/packages/at_auth/example/onboard_legacy.dart b/packages/at_auth/example/onboard_legacy.dart new file mode 100644 index 00000000..ed6acc83 --- /dev/null +++ b/packages/at_auth/example/onboard_legacy.dart @@ -0,0 +1,12 @@ +import 'package:at_auth/at_auth.dart'; + +/// dart onboard_legacy.dart +void main(List args) async { + final atAuth = AtAuthImpl(); + final atSign = args[0]; + final atOnboardingRequest = AtOnboardingRequest(atSign) + ..rootDomain = 'vip.ve.atsign.zone'; + final atOnboardingResponse = + await atAuth.onboard(atOnboardingRequest, args[1]); + print('atOnboardingResponse: $atOnboardingResponse'); +} diff --git a/packages/at_auth/lib/at_auth.dart b/packages/at_auth/lib/at_auth.dart new file mode 100644 index 00000000..c6d3ff0d --- /dev/null +++ b/packages/at_auth/lib/at_auth.dart @@ -0,0 +1,10 @@ +library at_auth; + +export 'src/at_auth_base.dart'; +export 'src/at_auth_impl.dart'; +export 'src/onboard/at_onboarding_request.dart'; +export 'src/onboard/at_onboarding_response.dart'; +export 'src/auth/at_auth_request.dart'; +export 'src/auth/at_auth_response.dart'; +export 'src/keys/at_auth_keys.dart'; +export 'src/exception/at_auth_exceptions.dart'; diff --git a/packages/at_auth/lib/src/at_auth_base.dart b/packages/at_auth/lib/src/at_auth_base.dart new file mode 100644 index 00000000..2c24dfc8 --- /dev/null +++ b/packages/at_auth/lib/src/at_auth_base.dart @@ -0,0 +1,25 @@ +import 'package:at_auth/src/onboard/at_onboarding_request.dart'; +import 'package:at_auth/src/onboard/at_onboarding_response.dart'; +import 'package:at_auth/src/auth/at_auth_request.dart'; +import 'package:at_auth/src/auth/at_auth_response.dart'; +import 'package:at_chops/at_chops.dart'; + +/// Interface for onboarding and authentication to a secondary server of an atsign +abstract class AtAuth { + AtChops? atChops; + + /// Authenticate method is invoked when an atsign wants to authenticate to secondary server with an .atKeys file + /// Step 1. Read the keys from [atAuthRequest.atAuthKeys] or [atAuthRequest.atKeysFilePath] + /// Step 2 Perform pkam authentication + Future authenticate(AtAuthRequest atAuthRequest); + + /// Onboard method is invoked when an atsign is activated for the first time from a client app. + /// Step 1. Perform cram auth + /// Step 2. Generate pkam, encryption keypairs and apkam symmetric key + /// Step 3. Update pkam public key to secondary + /// Step 4. Perform pkam auth + /// Step 5. Update encryption public key to server and delete cram secret from server + /// Set [atOnboardingRequest.publicKeyId] if pkam auth mode is [PkamAuthMode.sim] + Future onboard( + AtOnboardingRequest atOnboardingRequest, String cramSecret); +} diff --git a/packages/at_auth/lib/src/at_auth_impl.dart b/packages/at_auth/lib/src/at_auth_impl.dart new file mode 100644 index 00000000..9f3892d1 --- /dev/null +++ b/packages/at_auth/lib/src/at_auth_impl.dart @@ -0,0 +1,341 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:at_auth/src/at_auth_base.dart'; +import 'package:at_auth/src/auth/cram_authenticator.dart'; +import 'package:at_auth/src/auth/pkam_authenticator.dart'; +import 'package:at_auth/src/keys/at_auth_keys.dart'; +import 'package:at_auth/src/onboard/at_onboarding_request.dart'; +import 'package:at_auth/src/onboard/at_onboarding_response.dart'; +import 'package:at_auth/src/exception/at_auth_exceptions.dart'; +import 'package:at_auth/src/auth/at_auth_request.dart'; +import 'package:at_auth/src/auth/at_auth_response.dart'; +import 'package:at_auth/src/auth_constants.dart' as auth_constants; +import 'package:at_chops/at_chops.dart'; +import 'package:at_commons/at_builders.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; +import 'package:at_utils/at_logger.dart'; + +class AtAuthImpl implements AtAuth { + final AtSignLogger _logger = AtSignLogger('AtAuthServiceImpl'); + @override + AtChops? atChops; + + CramAuthenticator? cramAuthenticator; + + PkamAuthenticator? pkamAuthenticator; + + AtLookUp? atLookUp; + + AtAuthImpl( + {this.atLookUp, + this.atChops, + this.cramAuthenticator, + this.pkamAuthenticator}); + + @override + Future authenticate(AtAuthRequest atAuthRequest) async { + if (atAuthRequest.atKeysFilePath == null && + atAuthRequest.atAuthKeys == null) { + throw AtAuthenticationException( + 'Keyfile path or atAuthKeys object has to be set in atAuthRequest'); + } + // decrypts all the keys in .atKeysFile using the SelfEncryptionKey + // and stores the keys in a map + AtAuthKeys? atAuthKeys; + var enrollmentIdFromRequest = atAuthRequest.enrollmentId; + if (atAuthRequest.atKeysFilePath != null) { + atAuthKeys = _decryptAtKeysFile( + await _readAtKeysFile(atAuthRequest.atKeysFilePath), + atAuthRequest.authMode); + enrollmentIdFromRequest ??= atAuthKeys.enrollmentId; + } else { + atAuthKeys = atAuthRequest.atAuthKeys; + } + if (atAuthKeys == null) { + throw AtAuthenticationException( + 'AtAuthKeys is not set in the request/cannot be read from provided keysfile'); + } + var pkamPrivateKey = atAuthKeys.apkamPrivateKey; + + if (atAuthRequest.authMode == PkamAuthMode.keysFile && + pkamPrivateKey == null) { + throw AtPrivateKeyNotFoundException( + 'Unable to read PkamPrivateKey from provided atKeys file/atAuthKeys object', + exceptionScenario: ExceptionScenario.invalidValueProvided); + } + atLookUp ??= AtLookupImpl( + atAuthRequest.atSign, atAuthRequest.rootDomain, atAuthRequest.rootPort); + var atChops = _createAtChops(atAuthKeys); + this.atChops = atChops; + atLookUp!.atChops = atChops; + _logger.finer('Authenticating using PKAM'); + var isPkamAuthenticated = false; + pkamAuthenticator ??= PkamAuthenticator(atAuthRequest.atSign, atLookUp!); + try { + var pkamResponse = (await pkamAuthenticator! + .authenticate(enrollmentId: enrollmentIdFromRequest)); + isPkamAuthenticated = pkamResponse.isSuccessful; + } on Exception catch (e) { + _logger.severe('Caught exception: $e'); + throw AtAuthenticationException( + 'Unable to authenticate- ${e.toString()}'); + } + _logger.finer( + 'PKAM auth result: ${isPkamAuthenticated ? 'success' : 'failed'}'); + return AtAuthResponse(atAuthRequest.atSign) + ..isSuccessful = isPkamAuthenticated + ..enrollmentId = enrollmentIdFromRequest; + } + + @override + Future onboard( + AtOnboardingRequest atOnboardingRequest, String cramSecret) async { + var atOnboardingResponse = AtOnboardingResponse(atOnboardingRequest.atSign); + atLookUp ??= AtLookupImpl(atOnboardingRequest.atSign, + atOnboardingRequest.rootDomain, atOnboardingRequest.rootPort); + + //1. cram auth + cramAuthenticator ??= + CramAuthenticator(atOnboardingRequest.atSign, cramSecret, atLookUp); + var cramAuthResult = await cramAuthenticator!.authenticate(); + if (!cramAuthResult.isSuccessful) { + throw AtAuthenticationException( + 'Cram authentication failed. Please check the cram key' + ' and try again \n(or) contact support@atsign.com'); + } + //2. generate key pairs + var atAuthKeys = _generateKeyPairs(atOnboardingRequest.authMode, + publicKeyId: atOnboardingRequest.publicKeyId); + if (atChops == null) { + var atChops = _createAtChops(atAuthKeys); + this.atChops = atChops; + atLookUp!.atChops = atChops; + } + + //3. update pkam public key through enrollment or manually based on app preference + String? enrollmentIdFromServer; + if (atOnboardingRequest.enableEnrollment) { + // server will update the apkam public key during enrollment.So don't have to manually update in this scenario. + enrollmentIdFromServer = await _sendOnboardingEnrollment( + atOnboardingRequest, atAuthKeys, atLookUp!); + atAuthKeys.enrollmentId = enrollmentIdFromServer; + } else { + // update pkam public key to server if enrollment is not set in preference + _logger.finer('Updating PkamPublicKey to remote secondary'); + final pkamPublicKey = atAuthKeys.apkamPublicKey; + String updateCommand = 'update:$AT_PKAM_PUBLIC_KEY $pkamPublicKey\n'; + String? pkamUpdateResult = + await atLookUp!.executeCommand(updateCommand, auth: false); + _logger.finer('PkamPublicKey update result: $pkamUpdateResult'); + } + + //3. Close connection to server + try { + (atLookUp as AtLookupImpl).close(); + } on Exception catch (e) { + _logger.severe('error while closing connection to server: $e'); + } + + //4. Init _atLookUp again and attempt pkam auth + // atLookUp = AtLookupImpl(atOnboardingRequest.atSign, + // atOnboardingRequest.rootDomain, atOnboardingRequest.rootPort); + atLookUp!.atChops = atChops; + + var isPkamAuthenticated = false; + //5. Do pkam auth + pkamAuthenticator ??= + PkamAuthenticator(atOnboardingRequest.atSign, atLookUp!); + try { + var pkamResponse = await pkamAuthenticator! + .authenticate(enrollmentId: enrollmentIdFromServer); + isPkamAuthenticated = pkamResponse.isSuccessful; + } on UnAuthenticatedException catch (e) { + throw AtAuthenticationException('Pkam auth failed - $e '); + } + if (!isPkamAuthenticated) { + throw AtAuthenticationException('Pkam auth returned false'); + } + + //5. If Pkam auth is success, update encryption public key to secondary and delete cram key from server + final encryptionPublicKey = atAuthKeys.defaultEncryptionPublicKey; + UpdateVerbBuilder updateBuilder = UpdateVerbBuilder() + ..atKey = 'publickey' + ..isPublic = true + ..value = encryptionPublicKey + ..sharedBy = atOnboardingRequest.atSign; + String? encryptKeyUpdateResult = await atLookUp!.executeVerb(updateBuilder); + _logger.info('Encryption public key update result $encryptKeyUpdateResult'); + // deleting cram secret from the keystore as cram auth is complete + DeleteVerbBuilder deleteBuilder = DeleteVerbBuilder() + ..atKey = AT_CRAM_SECRET; + String? deleteResponse = await atLookUp!.executeVerb(deleteBuilder); + _logger.info('Cram secret delete response : $deleteResponse'); + atOnboardingResponse.isSuccessful = true; + atOnboardingResponse.enrollmentId = enrollmentIdFromServer; + atOnboardingResponse.atAuthKeys = atAuthKeys; + return atOnboardingResponse; + } + + AtChops _createAtChops(AtAuthKeys atKeysFile) { + final atEncryptionKeyPair = AtEncryptionKeyPair.create( + atKeysFile.defaultEncryptionPublicKey!, + atKeysFile.defaultEncryptionPrivateKey!); + final atPkamKeyPair = AtPkamKeyPair.create( + atKeysFile.apkamPublicKey!, atKeysFile.apkamPrivateKey!); + final atChopsKeys = AtChopsKeys.create(atEncryptionKeyPair, atPkamKeyPair); + if (atKeysFile.apkamSymmetricKey != null) { + atChopsKeys.apkamSymmetricKey = AESKey(atKeysFile.apkamSymmetricKey!); + } + atChopsKeys.selfEncryptionKey = + AESKey(atKeysFile.defaultSelfEncryptionKey!); + return AtChopsImpl(atChopsKeys); + } + + Future _sendOnboardingEnrollment( + AtOnboardingRequest atOnboardingRequest, + AtAuthKeys atAuthKeys, + AtLookUp atLookup) async { + var enrollBuilder = EnrollVerbBuilder() + ..appName = atOnboardingRequest.appName + ..deviceName = atOnboardingRequest.deviceName; + + var symmetricEncryptionAlgo = + AESEncryptionAlgo(AESKey(atAuthKeys.apkamSymmetricKey!)); + enrollBuilder.encryptedDefaultEncryptedPrivateKey = atChops! + .encryptString( + atAuthKeys.defaultEncryptionPrivateKey!, EncryptionKeyType.aes256, + encryptionAlgorithm: symmetricEncryptionAlgo, + iv: AtChopsUtil.generateIVLegacy()) + .result; + + enrollBuilder.encryptedDefaultSelfEncryptionKey = atChops! + .encryptString( + atAuthKeys.defaultSelfEncryptionKey!, EncryptionKeyType.aes256, + encryptionAlgorithm: symmetricEncryptionAlgo, + iv: AtChopsUtil.generateIVLegacy()) + .result; + enrollBuilder.apkamPublicKey = atAuthKeys.apkamPublicKey; + + var enrollResult = await atLookup + .executeCommand(enrollBuilder.buildCommand(), auth: false); + if (enrollResult == null || enrollResult.isEmpty) { + throw AtAuthenticationException('Enrollment response is null or empty'); + } else if (enrollResult.startsWith('error:')) { + throw AtAuthenticationException('Enrollment error:$enrollResult'); + } + enrollResult = enrollResult.replaceFirst('data:', ''); + _logger.finer('enrollResult: $enrollResult'); + var enrollResultJson = jsonDecode(enrollResult); + var enrollmentIdFromServer = enrollResultJson[enrollmentId]; + var enrollmentStatus = enrollResultJson['status']; + if (enrollmentStatus != 'approved') { + throw AtAuthenticationException( + 'initial enrollment is not approved. Status from server: $enrollmentStatus'); + } + return enrollmentIdFromServer; + } + + AtAuthKeys _decryptAtKeysFile( + Map jsonData, PkamAuthMode authMode) { + var securityKeys = AtAuthKeys(); + String decryptionKey = jsonData[auth_constants.defaultSelfEncryptionKey]!; + var atChops = + AtChopsImpl(AtChopsKeys()..selfEncryptionKey = AESKey(decryptionKey)); + securityKeys.defaultEncryptionPublicKey = atChops + .decryptString(jsonData[auth_constants.defaultEncryptionPublicKey]!, + EncryptionKeyType.aes256, + keyName: 'selfEncryptionKey', iv: AtChopsUtil.generateIVLegacy()) + .result; + securityKeys.defaultEncryptionPrivateKey = atChops + .decryptString(jsonData[auth_constants.defaultEncryptionPrivateKey]!, + EncryptionKeyType.aes256, + keyName: 'selfEncryptionKey', iv: AtChopsUtil.generateIVLegacy()) + .result; + securityKeys.defaultSelfEncryptionKey = decryptionKey; + securityKeys.apkamPublicKey = atChops + .decryptString( + jsonData[auth_constants.apkamPublicKey]!, EncryptionKeyType.aes256, + keyName: 'selfEncryptionKey', iv: AtChopsUtil.generateIVLegacy()) + .result; + // 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 (authMode == PkamAuthMode.keysFile) { + securityKeys.apkamPrivateKey = atChops + .decryptString(jsonData[auth_constants.apkamPrivateKey]!, + EncryptionKeyType.aes256, + keyName: 'selfEncryptionKey', iv: AtChopsUtil.generateIVLegacy()) + .result; + } + securityKeys.apkamSymmetricKey = jsonData[auth_constants.apkamSymmetricKey]; + securityKeys.enrollmentId = jsonData[enrollmentId]; + return securityKeys; + } + + ///method to read and return data from .atKeysFile + ///returns map containing encryption keys + Future> _readAtKeysFile(String? atKeysFilePath) async { + if (atKeysFilePath == null || atKeysFilePath.isEmpty) { + throw AtException( + 'atKeys filePath is empty. atKeysFile is required to authenticate'); + } + if (!File(atKeysFilePath).existsSync()) { + throw AtException( + 'provided keys file doesnot exists. Please check whether the file path $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; + } + + AtAuthKeys _generateKeyPairs(PkamAuthMode authMode, {String? publicKeyId}) { + // generate user encryption keypair + _logger.info('Generating encryption keypair'); + var atEncryptionKeyPair = AtChopsUtil.generateAtEncryptionKeyPair(); + + //generate selfEncryptionKey + var selfEncryptionKey = + AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); + var apkamSymmetricKey = + AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); + var atKeysFile = AtAuthKeys(); + _logger.info( + '[Information] Generating your encryption keys and .atKeys file\n'); + //generating pkamKeyPair only if authMode is keysFile + String? pkamPublicKey; + if (authMode == PkamAuthMode.keysFile) { + _logger.info('Generating pkam keypair'); + var apkamRsaKeypair = AtChopsUtil.generateAtPkamKeyPair(); + pkamPublicKey = apkamRsaKeypair.atPublicKey.publicKey.toString(); + atKeysFile.apkamPrivateKey = + apkamRsaKeypair.atPrivateKey.privateKey.toString(); + } else if (authMode == PkamAuthMode.sim) { + // get the public key from secure element + pkamPublicKey = atChops!.readPublicKey(publicKeyId!); + _logger.info('pkam public key from sim: ${atKeysFile.apkamPublicKey}'); + + // encryption key pair and self encryption symmetric key + // are not available to injected at_chops. Set it here + atChops!.atChopsKeys.atEncryptionKeyPair = atEncryptionKeyPair; + atChops!.atChopsKeys.selfEncryptionKey = selfEncryptionKey; + atChops!.atChopsKeys.apkamSymmetricKey = apkamSymmetricKey; + } + atKeysFile.apkamPublicKey = pkamPublicKey; + //Standard order of an atKeys file is -> + // pkam keypair -> encryption keypair -> selfEncryption key -> enrollmentId --> apkam symmetric key --> + // @sign: selfEncryptionKey[self encryption key again] + // note: "->" stands for "followed by" + atKeysFile.defaultEncryptionPublicKey = + atEncryptionKeyPair.atPublicKey.publicKey.toString(); + atKeysFile.defaultEncryptionPrivateKey = + atEncryptionKeyPair.atPrivateKey.privateKey.toString(); + atKeysFile.defaultSelfEncryptionKey = selfEncryptionKey.key; + atKeysFile.apkamSymmetricKey = apkamSymmetricKey.key; + + return atKeysFile; + } +} diff --git a/packages/at_auth/lib/src/auth/at_auth_request.dart b/packages/at_auth/lib/src/auth/at_auth_request.dart new file mode 100644 index 00000000..d3f85238 --- /dev/null +++ b/packages/at_auth/lib/src/auth/at_auth_request.dart @@ -0,0 +1,16 @@ +import 'package:at_auth/src/keys/at_auth_keys.dart'; +import 'package:at_commons/at_commons.dart'; + +class AtAuthRequest { + String atSign; + AtAuthRequest(this.atSign); + String? enrollmentId; + AtAuthKeys? atAuthKeys; + String rootDomain = 'root.atsign.org'; + int rootPort = 64; + PkamAuthMode authMode = PkamAuthMode.keysFile; + String? atKeysFilePath; + + /// public key id from secure element if [authMode] is [PkamAuthMode.sim] + String? publicKeyId; +} diff --git a/packages/at_auth/lib/src/auth/at_auth_response.dart b/packages/at_auth/lib/src/auth/at_auth_response.dart new file mode 100644 index 00000000..de47bf6e --- /dev/null +++ b/packages/at_auth/lib/src/auth/at_auth_response.dart @@ -0,0 +1,11 @@ +class AtAuthResponse { + String atSign; + AtAuthResponse(this.atSign); + bool isSuccessful = false; + String? enrollmentId; + + @override + String toString() { + return 'AtAuthResponse{atSign: $atSign, enrollmentId: $enrollmentId, isSuccessful: $isSuccessful}'; + } +} diff --git a/packages/at_auth/lib/src/auth/cram_authenticator.dart b/packages/at_auth/lib/src/auth/cram_authenticator.dart new file mode 100644 index 00000000..8568c238 --- /dev/null +++ b/packages/at_auth/lib/src/auth/cram_authenticator.dart @@ -0,0 +1,24 @@ +import 'package:at_auth/src/auth/at_auth_response.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; + +class CramAuthenticator { + final String _cramSecret; + final String _atSign; + CramAuthenticator(this._atSign, this._cramSecret, this.atLookup); + + AtLookUp? atLookup; + + Future authenticate() async { + var authResult = AtAuthResponse(_atSign); + try { + bool cramResult = + await (atLookup as AtLookupImpl).authenticate_cram(_cramSecret); + authResult.isSuccessful = cramResult; + } on UnAuthenticatedException catch (e) { + throw UnAuthenticatedException( + 'cram auth failed for $_atSign - ${e.toString()}'); + } + return authResult; + } +} diff --git a/packages/at_auth/lib/src/auth/pkam_authenticator.dart b/packages/at_auth/lib/src/auth/pkam_authenticator.dart new file mode 100644 index 00000000..af97be7a --- /dev/null +++ b/packages/at_auth/lib/src/auth/pkam_authenticator.dart @@ -0,0 +1,22 @@ +import 'package:at_auth/src/auth/at_auth_response.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; + +class PkamAuthenticator { + final String _atSign; + final AtLookUp _atLookup; + PkamAuthenticator(this._atSign, this._atLookup); + + Future authenticate({String? enrollmentId}) async { + var authResult = AtAuthResponse(_atSign); + try { + bool pkamResult = + await _atLookup.pkamAuthenticate(enrollmentId: enrollmentId); + authResult.isSuccessful = pkamResult; + } on UnAuthenticatedException catch (e) { + throw UnAuthenticatedException( + 'pkam auth failed for $_atSign - ${e.toString()}'); + } + return authResult; + } +} diff --git a/packages/at_auth/lib/src/auth_constants.dart b/packages/at_auth/lib/src/auth_constants.dart new file mode 100644 index 00000000..e792a859 --- /dev/null +++ b/packages/at_auth/lib/src/auth_constants.dart @@ -0,0 +1,7 @@ +const String apkamPublicKey = 'aesPkamPublicKey'; +const String apkamPrivateKey = 'aesPkamPrivateKey'; +const String defaultEncryptionPublicKey = 'aesEncryptPublicKey'; +const String defaultEncryptionPrivateKey = 'aesEncryptPrivateKey'; +const String defaultSelfEncryptionKey = 'selfEncryptionKey'; +const String apkamSymmetricKey = 'apkamSymmetricKey'; +const String apkamEnrollmentId = 'enrollmentId'; diff --git a/packages/at_auth/lib/src/exception/at_auth_exceptions.dart b/packages/at_auth/lib/src/exception/at_auth_exceptions.dart new file mode 100644 index 00000000..4c53c7bc --- /dev/null +++ b/packages/at_auth/lib/src/exception/at_auth_exceptions.dart @@ -0,0 +1,10 @@ +import 'package:at_commons/at_commons.dart'; + +// commenting since there is a conflict with same exception in at_onboarding_cli +// class AtOnboardingException extends AtException { +// AtOnboardingException(super.message); +// } + +class AtAuthenticationException extends AtException { + AtAuthenticationException(super.message); +} diff --git a/packages/at_auth/lib/src/keys/at_auth_keys.dart b/packages/at_auth/lib/src/keys/at_auth_keys.dart new file mode 100644 index 00000000..9c35b2a5 --- /dev/null +++ b/packages/at_auth/lib/src/keys/at_auth_keys.dart @@ -0,0 +1,28 @@ +import 'package:at_auth/src/auth_constants.dart' as auth_constants; + +/// Holder for different encryption keys that will be stored in .atKeys file. +/// Apkam symmetric key, enrollmentId and defaultSelfEncryptionKey will be stored in unencrypted format in .atKeys file. +/// All other values will be encrypted before saving to .atKeys file. +class AtAuthKeys { + String? apkamPublicKey; + String? apkamPrivateKey; + String? defaultEncryptionPublicKey; + String? defaultEncryptionPrivateKey; + String? defaultSelfEncryptionKey; + String? apkamSymmetricKey; + String? enrollmentId; + + Map toMap() { + var keysMap = {}; + keysMap[auth_constants.apkamPrivateKey] = apkamPrivateKey; + keysMap[auth_constants.apkamPublicKey] = apkamPublicKey; + keysMap[auth_constants.defaultEncryptionPrivateKey] = + defaultEncryptionPrivateKey; + keysMap[auth_constants.defaultEncryptionPublicKey] = + defaultEncryptionPublicKey; + keysMap[auth_constants.defaultSelfEncryptionKey] = defaultSelfEncryptionKey; + keysMap[auth_constants.apkamSymmetricKey] = apkamSymmetricKey; + keysMap[auth_constants.apkamEnrollmentId] = enrollmentId; + return keysMap; + } +} diff --git a/packages/at_auth/lib/src/onboard/at_onboarding_request.dart b/packages/at_auth/lib/src/onboard/at_onboarding_request.dart new file mode 100644 index 00000000..e1371112 --- /dev/null +++ b/packages/at_auth/lib/src/onboard/at_onboarding_request.dart @@ -0,0 +1,15 @@ +import 'package:at_commons/at_commons.dart'; + +class AtOnboardingRequest { + String atSign; + AtOnboardingRequest(this.atSign); + PkamAuthMode authMode = PkamAuthMode.keysFile; + bool enableEnrollment = false; + String rootDomain = 'root.atsign.org'; + int rootPort = 64; + String? appName; + String? deviceName; + + /// public key id if [authMode] is [PkamAuthMode.sim] + String? publicKeyId; +} diff --git a/packages/at_auth/lib/src/onboard/at_onboarding_response.dart b/packages/at_auth/lib/src/onboard/at_onboarding_response.dart new file mode 100644 index 00000000..28c422dd --- /dev/null +++ b/packages/at_auth/lib/src/onboard/at_onboarding_response.dart @@ -0,0 +1,14 @@ +import 'package:at_auth/src/keys/at_auth_keys.dart'; + +class AtOnboardingResponse { + String atSign; + String? enrollmentId; + AtOnboardingResponse(this.atSign); + bool isSuccessful = false; + AtAuthKeys? atAuthKeys; + + @override + String toString() { + return 'AtOnboardingResponse{atSign: $atSign, enrollmentId: $enrollmentId, isSuccessful: $isSuccessful}'; + } +} diff --git a/packages/at_auth/pubspec.yaml b/packages/at_auth/pubspec.yaml new file mode 100644 index 00000000..1429a31a --- /dev/null +++ b/packages/at_auth/pubspec.yaml @@ -0,0 +1,22 @@ +name: at_auth +description: Package that implements common logic for onboarding/authenticating an atsign to a secondary server +version: 1.0.0 +homepage: https://atsign.com/ +repository: https://github.com/atsign-foundation/at_libraries + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + at_commons: ^3.0.55 + at_lookup: ^3.0.40 + at_chops: ^1.0.4 + at_utils: ^3.0.15 + +dependency_overrides: + at_chops: + path: ../at_chops +dev_dependencies: + lints: ^2.0.0 + test: ^1.24.7 + mocktail: ^0.3.0 diff --git a/packages/at_auth/test/at_auth_test.dart b/packages/at_auth/test/at_auth_test.dart new file mode 100644 index 00000000..bc7e6e9c --- /dev/null +++ b/packages/at_auth/test/at_auth_test.dart @@ -0,0 +1,259 @@ +import 'package:at_auth/src/auth/pkam_authenticator.dart'; +import 'package:at_chops/at_chops.dart'; +import 'package:at_commons/at_builders.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:at_auth/at_auth.dart'; + +// Create a mock for AtLookUp +class MockAtLookUp extends Mock implements AtLookupImpl {} + +// Create a mock for AtChops +class MockAtChops extends Mock implements AtChops {} + +class MockPkamAuthenticator extends Mock implements PkamAuthenticator {} + +class FakeUpdateVerbBuilder extends Fake implements UpdateVerbBuilder {} + +class FakeDeleteVerbBuilder extends Fake implements DeleteVerbBuilder {} + +class FakeVerbBuilder extends Fake implements VerbBuilder {} + +void main() { + late AtAuthImpl atAuth; + late MockAtLookUp mockAtLookUp; + late MockPkamAuthenticator mockPkamAuthenticator; + final String testEnrollmentId = '352b78c8-4b6f-4d07-a9cf-5466512ffa44'; + + setUp(() { + mockAtLookUp = MockAtLookUp(); + mockPkamAuthenticator = MockPkamAuthenticator(); + atAuth = AtAuthImpl( + atLookUp: mockAtLookUp, pkamAuthenticator: mockPkamAuthenticator); + registerFallbackValue(FakeVerbBuilder()); + }); + group('AtAuthImpl authentication tests', () { + test('Test authenticate() true with keys file', () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) => Future.value(true)); + when(() => + mockPkamAuthenticator.authenticate( + enrollmentId: testEnrollmentId)).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + atAuthRequest.atKeysFilePath = 'test/data/@aliceđź› _key.atKeys'; + + final response = await atAuth.authenticate(atAuthRequest); + + expect(response.isSuccessful, true); + expect(response.enrollmentId, testEnrollmentId); + }); + + test('Test authenticate() false with keys file', () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) => Future.value(false)); + when(() => mockPkamAuthenticator.authenticate( + enrollmentId: testEnrollmentId)) + .thenAnswer((_) => + Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = false)); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + atAuthRequest.atKeysFilePath = 'test/data/@aliceđź› _key.atKeys'; + + final response = await atAuth.authenticate(atAuthRequest); + + expect(response.isSuccessful, false); + expect(response.enrollmentId, testEnrollmentId); + }); + + test('Test authenticate() invalid keys file path', () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) => Future.value(true)); + when(() => + mockPkamAuthenticator.authenticate( + enrollmentId: testEnrollmentId)).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + atAuthRequest.atKeysFilePath = 'test/data/hello/@aliceđź› _key.atKeys'; + + expect(() async => await atAuth.authenticate(atAuthRequest), + throwsA(isA())); + }); + + test('Test authenticate() with atAuthKeys set', () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) => Future.value(true)); + when(() => + mockPkamAuthenticator.authenticate( + enrollmentId: testEnrollmentId)).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + atAuthRequest.atAuthKeys = AtAuthKeys() + ..apkamPublicKey = 'testApkamPublicKey' + ..apkamPrivateKey = 'testApkamPrivateKey' + ..defaultEncryptionPublicKey = 'defaultEncryptionPublicKey' + ..defaultEncryptionPrivateKey = 'defaultEncryptionPrivateKey' + ..defaultSelfEncryptionKey = 'defaultSelfEncryptionKey' + ..enrollmentId = testEnrollmentId; + + final response = await atAuth.authenticate(atAuthRequest); + + expect(response.isSuccessful, true); + expect(response.enrollmentId, testEnrollmentId); + }); + + test( + 'Test authenticate() - throw exception is pkamPrivateKey is not set for default auth mode.', + () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) => Future.value(true)); + when(() => + mockPkamAuthenticator.authenticate( + enrollmentId: testEnrollmentId)).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + atAuthRequest.atAuthKeys = AtAuthKeys() + ..apkamPublicKey = 'testApkamPublicKey' + ..defaultEncryptionPublicKey = 'defaultEncryptionPublicKey' + ..defaultEncryptionPrivateKey = 'defaultEncryptionPrivateKey' + ..defaultSelfEncryptionKey = 'defaultSelfEncryptionKey' + ..enrollmentId = testEnrollmentId; + + expect(() async => await atAuth.authenticate(atAuthRequest), + throwsA(isA())); + }); + + test( + 'Test authenticate throws exception when keysfile path and atAuthKeys is not set in request', + () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) => Future.value(true)); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + + expect(() async => await atAuth.authenticate(atAuthRequest), + throwsA(isA())); + }); + + test( + 'Test authenticate() pkamAuthenticate method throws UnAuthenticatedException', + () async { + when(() => mockAtLookUp.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenThrow(UnAuthenticatedException('Unauthenticated')); + when(() => mockPkamAuthenticator.authenticate( + enrollmentId: testEnrollmentId)) + .thenThrow(AtAuthenticationException('Unauthenticated')); + final atAuthRequest = AtAuthRequest('@aliceđź› '); + atAuthRequest.enrollmentId = testEnrollmentId; + atAuthRequest.atKeysFilePath = 'test/data/@aliceđź› _key.atKeys'; + + expect(() async => await atAuth.authenticate(atAuthRequest), + throwsA(isA())); + }); + }); + group('AtAuthImpl onboarding tests', () { + var testCramSecret = 'cram123'; + test('Test onboard - authenticate_cram returns true', () async { + when(() => mockAtLookUp.authenticate_cram(testCramSecret)) + .thenAnswer((_) => Future.value(true)); + when(() => mockAtLookUp.executeCommand(any())) + .thenAnswer((_) => Future.value('data:1')); + when(() => mockAtLookUp.executeVerb(any())) + .thenAnswer((_) => Future.value('data:2')); + + when(() => mockAtLookUp.close()).thenAnswer((_) async => {}); + when(() => mockPkamAuthenticator.authenticate()).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + + final atOnboardingRequest = AtOnboardingRequest('@aliceđź› ') + ..rootDomain = 'test.atsign.com' + ..rootPort = 64; + + final response = + await atAuth.onboard(atOnboardingRequest, testCramSecret); + + expect(response.isSuccessful, true); + }); + test('Test onboard - authenticate_cram returns false', () async { + when(() => mockAtLookUp.authenticate_cram(testCramSecret)) + .thenAnswer((_) => Future.value(false)); + when(() => mockAtLookUp.executeCommand(any())) + .thenAnswer((_) => Future.value('data:1')); + when(() => mockAtLookUp.executeVerb(any())) + .thenAnswer((_) => Future.value('data:2')); + + when(() => mockAtLookUp.close()).thenAnswer((_) async => {}); + when(() => mockPkamAuthenticator.authenticate()).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + + final atOnboardingRequest = AtOnboardingRequest('@aliceđź› ') + ..rootDomain = 'test.atsign.com' + ..rootPort = 64; + + expect( + () async => await atAuth.onboard(atOnboardingRequest, testCramSecret), + throwsA(isA())); + }); + + test('Test onboard - enable enrollment', () async { + when(() => mockAtLookUp.authenticate_cram(testCramSecret)) + .thenAnswer((_) => Future.value(true)); + var mockEnrollResponse = + 'data:{"enrollmentId":"abc123","status":"approved"}'; + when(() => mockAtLookUp.executeCommand(any(that: startsWith('enroll:')))) + .thenAnswer((_) => Future.value(mockEnrollResponse)); + when(() => mockAtLookUp.executeVerb(any())) + .thenAnswer((_) => Future.value('data:2')); + + when(() => mockAtLookUp.close()).thenAnswer((_) async => {}); + when(() => mockPkamAuthenticator.authenticate(enrollmentId: "abc123")) + .thenAnswer((_) => + Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + + final atOnboardingRequest = AtOnboardingRequest('@aliceđź› ') + ..rootDomain = 'test.atsign.com' + ..rootPort = 64 + ..enableEnrollment = true + ..appName = 'wavi' + ..authMode = PkamAuthMode.keysFile + ..deviceName = 'iphone'; + + final response = + await atAuth.onboard(atOnboardingRequest, testCramSecret); + + expect(response.isSuccessful, true); + expect(response.enrollmentId, 'abc123'); + }); + + test('Test onboard - enable enrollment set to false', () async { + when(() => mockAtLookUp.authenticate_cram(testCramSecret)) + .thenAnswer((_) => Future.value(true)); + when(() => mockAtLookUp.executeCommand(any())) + .thenAnswer((_) => Future.value('data:1')); + when(() => mockAtLookUp.executeVerb(any())) + .thenAnswer((_) => Future.value('data:2')); + + when(() => mockAtLookUp.close()).thenAnswer((_) async => {}); + when(() => mockPkamAuthenticator.authenticate()).thenAnswer( + (_) => Future.value(AtAuthResponse('@aliceđź› ')..isSuccessful = true)); + + final atOnboardingRequest = AtOnboardingRequest('@aliceđź› ') + ..rootDomain = 'test.atsign.com' + ..rootPort = 64 + ..enableEnrollment = false + ..appName = 'wavi' + ..deviceName = 'iphone'; + + final response = + await atAuth.onboard(atOnboardingRequest, testCramSecret); + expect(response.isSuccessful, true); + }); + + }); +} diff --git a/packages/at_auth/test/cram_authenticator_test.dart b/packages/at_auth/test/cram_authenticator_test.dart new file mode 100644 index 00000000..d80a935a --- /dev/null +++ b/packages/at_auth/test/cram_authenticator_test.dart @@ -0,0 +1,41 @@ +import 'package:at_auth/src/auth/at_auth_response.dart'; +import 'package:at_auth/src/auth/cram_authenticator.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAtLookupImpl extends Mock implements AtLookupImpl {} + +void main() { + group('CramAuthenticator tests', () { + late CramAuthenticator cramAuthenticator; + late MockAtLookupImpl mockAtLookup; + final String atSign = '@alice'; + final String cramSecret = 'testCramSecret'; + + setUp(() { + mockAtLookup = MockAtLookupImpl(); + cramAuthenticator = CramAuthenticator(atSign, cramSecret, mockAtLookup); + }); + + test('authenticate() should return a successful AtAuthResponse', () async { + when(() => mockAtLookup.authenticate_cram(cramSecret)) + .thenAnswer((_) async => true); + + final result = await cramAuthenticator.authenticate(); + + expect(result, isA()); + expect(result.isSuccessful, isTrue); + }); + + test('authenticate() should throw UnAuthenticatedException on failure', + () async { + when(() => mockAtLookup.authenticate_cram(cramSecret)) + .thenThrow(UnAuthenticatedException('Unauthenticated')); + + expect(() async => await cramAuthenticator.authenticate(), + throwsA(isA())); + }); + }); +} diff --git "a/packages/at_auth/test/data/@alice\360\237\233\240_key.atKeys" "b/packages/at_auth/test/data/@alice\360\237\233\240_key.atKeys" new file mode 100644 index 00000000..6dacb2bd --- /dev/null +++ "b/packages/at_auth/test/data/@alice\360\237\233\240_key.atKeys" @@ -0,0 +1 @@ +{"aesPkamPublicKey":"r1xqwZsnvEqdwPmKwXuageb6DSr3YfveRqHDBnsiG6mkH0vT1vcFxLrIjMKC3G39N9mQ9tpSKOa+FCIQrn4O32I8Rn1v2FE+cIErqkTnp/q+1BK+/f1vOiEFcoW5To2LCglO2G/2rR2QGplPdpvcbRYq//5rMGYM6KpesqbNH/rlK8d8TILix+pvfYycB6k0o7NF6DPBWy8D96372mZpTHWeLjU3aDNhYvhQ0kxZHliiemPwNK3kcHMWfZBxndcJXJfrCQErUNiPim3hPsuocGmqyzHBYl0cYCkrCNKcpPEOXf5SnvAqM1kUjNKNxYwJXrqJLhidCmflVukkb0dv+qDkQSMZFmMVYVdbmirDO3zxFps4Ru7/DHus/MIGkNj5eyFqN+3nuyKrIQR0PXW4AmJ7MP/gYLWOfaLQBgs8EranFaeXtRfoaD95Wadp+yyljUj+u4LU7H17IGwFnT6C2+OXNHSuo+3WaiYpXew5MqgeZkcpcQOK6te4nVEF0RCNh46BQn+0dQTPLxK7KYW/8A==","aesEncryptPublicKey":"r1xqwZsnvEqdwPmKwXuageb6DSr3YfveRqHDBnsiG6mkH0vT1vcFxLrIjMKfyjioCMi56/NWEYiebRMihlkAnjhJez5loU57Vd83pmG64/ietxfX2dQ/F14JLJOQCuq8XAVx7nSo5CLKGJMoX4nHLi4Q3/RILWs39Zxd8/yAJ8vgPOtnZvjPxZgRY5GNaqoAgulnh1b/DCgw4/C6xGBlMF+3AEtJJBpHf4Z42GhoCXS5d0HQI8rEd3YWYZ9xhOoPUMb/SzxDKs2vo1HQad+PeFqL0Sj5bEEtd1R9IvnMufVcWPJKucZSOh0Ru5mxvsx+PPuJNBrKXD/Nft8yP0oclqTlX38EOkMzfVMWiAzmGlvsCd8yH+jzL2ag+OkurebeSGl/Coe7iDvSInEnZETVHgFDGIPnf5WnX6HMKFcCD7CvNYSwiB3IYWRRO7VGwjCD7S/yu5e77GVvaEEEr3Dwu/69H1Ktq+yLYnVTb9wfI4VlZRg8RDzk34fijGMA6xWsv6iBQn+0dQTPLxK7KYW/8A==","aesEncryptPrivateKey":"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","selfEncryptionKey":"UaQFluEFmCwC3mq/BzE8dKotSY1YUJwMIFVeaSycNGA=","@aliceđź› ":"UaQFluEFmCwC3mq/BzE8dKotSY1YUJwMIFVeaSycNGA=","apkamSymmetricKey":"WCxZ+tRsKktQM3YpNb5vT4Q/9lIGxfUmQSJEVnedZSI=","enrollmentId":"352b78c8-4b6f-4d07-a9cf-5466512ffa44","aesPkamPrivateKey":"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"} \ No newline at end of file diff --git a/packages/at_auth/test/pkam_authenticator_test.dart b/packages/at_auth/test/pkam_authenticator_test.dart new file mode 100644 index 00000000..981595d1 --- /dev/null +++ b/packages/at_auth/test/pkam_authenticator_test.dart @@ -0,0 +1,44 @@ +import 'package:at_auth/src/auth/at_auth_response.dart'; +import 'package:at_auth/src/auth/pkam_authenticator.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAtLookup extends Mock implements AtLookUp {} + +void main() { + group('PkamAuthenticator tests', () { + late PkamAuthenticator pkamAuthenticator; + late MockAtLookup mockAtLookup; + final String atSign = '@alice'; + final String testEnrollmentId = 'testEnrollmentId'; + + setUp(() { + mockAtLookup = MockAtLookup(); + pkamAuthenticator = PkamAuthenticator(atSign, mockAtLookup); + }); + + test('authenticate() should return a successful AtAuthResponse', () async { + when(() => mockAtLookup.pkamAuthenticate(enrollmentId: testEnrollmentId)) + .thenAnswer((_) async => true); + + final result = + await pkamAuthenticator.authenticate(enrollmentId: testEnrollmentId); + + expect(result, isA()); + expect(result.isSuccessful, isTrue); + }); + + test('authenticate() should throw UnAuthenticatedException on failure', + () async { + when(() => mockAtLookup.pkamAuthenticate(enrollmentId: enrollmentId)) + .thenThrow(UnAuthenticatedException('Unauthenticated')); + + expect( + () async => + await pkamAuthenticator.authenticate(enrollmentId: enrollmentId), + throwsA(isA())); + }); + }); +} From b9a374928f1052a4a389714b4c2d6588f7150a04 Mon Sep 17 00:00:00 2001 From: murali-shris Date: Fri, 13 Oct 2023 13:17:29 +0530 Subject: [PATCH 2/6] fix: added at_chops in dependency overrides --- packages/at_auth/pubspec.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/at_auth/pubspec.yaml b/packages/at_auth/pubspec.yaml index 1429a31a..2d5d00e1 100644 --- a/packages/at_auth/pubspec.yaml +++ b/packages/at_auth/pubspec.yaml @@ -13,9 +13,14 @@ dependencies: at_chops: ^1.0.4 at_utils: ^3.0.15 +#TODO replace with published version dependency_overrides: at_chops: - path: ../at_chops + git: + url: https://github.com/atsign-foundation/at_libraries + path: packages/at_chops + ref: at_chops_changes_at_auth + dev_dependencies: lints: ^2.0.0 test: ^1.24.7 From f8d966be58e6e32e40a5cce8400024041e5356e4 Mon Sep 17 00:00:00 2001 From: gkc Date: Fri, 13 Oct 2023 10:56:47 +0100 Subject: [PATCH 3/6] build: remove at_chops dependency override and use published version 1.0.5 --- packages/at_auth/pubspec.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/at_auth/pubspec.yaml b/packages/at_auth/pubspec.yaml index 2d5d00e1..a3a93a13 100644 --- a/packages/at_auth/pubspec.yaml +++ b/packages/at_auth/pubspec.yaml @@ -10,17 +10,9 @@ environment: dependencies: at_commons: ^3.0.55 at_lookup: ^3.0.40 - at_chops: ^1.0.4 + at_chops: ^1.0.5 at_utils: ^3.0.15 -#TODO replace with published version -dependency_overrides: - at_chops: - git: - url: https://github.com/atsign-foundation/at_libraries - path: packages/at_chops - ref: at_chops_changes_at_auth - dev_dependencies: lints: ^2.0.0 test: ^1.24.7 From ed8ab1347d057adcc63b0181b8e5c83088485232 Mon Sep 17 00:00:00 2001 From: gkc Date: Fri, 13 Oct 2023 10:57:23 +0100 Subject: [PATCH 4/6] refactor: fix AtConstants-related deprecation lint --- packages/at_auth/lib/src/at_auth_impl.dart | 12 ++++++------ packages/at_auth/test/pkam_authenticator_test.dart | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/at_auth/lib/src/at_auth_impl.dart b/packages/at_auth/lib/src/at_auth_impl.dart index 9f3892d1..f5e48247 100644 --- a/packages/at_auth/lib/src/at_auth_impl.dart +++ b/packages/at_auth/lib/src/at_auth_impl.dart @@ -55,7 +55,7 @@ class AtAuthImpl implements AtAuth { } if (atAuthKeys == null) { throw AtAuthenticationException( - 'AtAuthKeys is not set in the request/cannot be read from provided keysfile'); + 'keys either were not provided in the AtAuthRequest, or could not be read from provided keys file'); } var pkamPrivateKey = atAuthKeys.apkamPrivateKey; @@ -125,7 +125,7 @@ class AtAuthImpl implements AtAuth { // update pkam public key to server if enrollment is not set in preference _logger.finer('Updating PkamPublicKey to remote secondary'); final pkamPublicKey = atAuthKeys.apkamPublicKey; - String updateCommand = 'update:$AT_PKAM_PUBLIC_KEY $pkamPublicKey\n'; + String updateCommand = 'update:${AtConstants.atPkamPublicKey} $pkamPublicKey\n'; String? pkamUpdateResult = await atLookUp!.executeCommand(updateCommand, auth: false); _logger.finer('PkamPublicKey update result: $pkamUpdateResult'); @@ -169,7 +169,7 @@ class AtAuthImpl implements AtAuth { _logger.info('Encryption public key update result $encryptKeyUpdateResult'); // deleting cram secret from the keystore as cram auth is complete DeleteVerbBuilder deleteBuilder = DeleteVerbBuilder() - ..atKey = AT_CRAM_SECRET; + ..atKey = AtConstants.atCramSecret; String? deleteResponse = await atLookUp!.executeVerb(deleteBuilder); _logger.info('Cram secret delete response : $deleteResponse'); atOnboardingResponse.isSuccessful = true; @@ -228,7 +228,7 @@ class AtAuthImpl implements AtAuth { enrollResult = enrollResult.replaceFirst('data:', ''); _logger.finer('enrollResult: $enrollResult'); var enrollResultJson = jsonDecode(enrollResult); - var enrollmentIdFromServer = enrollResultJson[enrollmentId]; + var enrollmentIdFromServer = enrollResultJson[AtConstants.enrollmentId]; var enrollmentStatus = enrollResultJson['status']; if (enrollmentStatus != 'approved') { throw AtAuthenticationException( @@ -269,7 +269,7 @@ class AtAuthImpl implements AtAuth { .result; } securityKeys.apkamSymmetricKey = jsonData[auth_constants.apkamSymmetricKey]; - securityKeys.enrollmentId = jsonData[enrollmentId]; + securityKeys.enrollmentId = jsonData[AtConstants.enrollmentId]; return securityKeys; } @@ -282,7 +282,7 @@ class AtAuthImpl implements AtAuth { } if (!File(atKeysFilePath).existsSync()) { throw AtException( - 'provided keys file doesnot exists. Please check whether the file path $atKeysFilePath is valid'); + 'provided keys file does not exist. Please check whether the file path $atKeysFilePath is valid'); } String atAuthData = await File(atKeysFilePath).readAsString(); Map jsonData = {}; diff --git a/packages/at_auth/test/pkam_authenticator_test.dart b/packages/at_auth/test/pkam_authenticator_test.dart index 981595d1..ae9b0fb3 100644 --- a/packages/at_auth/test/pkam_authenticator_test.dart +++ b/packages/at_auth/test/pkam_authenticator_test.dart @@ -32,12 +32,12 @@ void main() { test('authenticate() should throw UnAuthenticatedException on failure', () async { - when(() => mockAtLookup.pkamAuthenticate(enrollmentId: enrollmentId)) + when(() => mockAtLookup.pkamAuthenticate(enrollmentId: AtConstants.enrollmentId)) .thenThrow(UnAuthenticatedException('Unauthenticated')); expect( () async => - await pkamAuthenticator.authenticate(enrollmentId: enrollmentId), + await pkamAuthenticator.authenticate(enrollmentId: AtConstants.enrollmentId), throwsA(isA())); }); }); From db6c671c22310d91c68d4a48ea2c6011fa8823c6 Mon Sep 17 00:00:00 2001 From: gkc Date: Fri, 13 Oct 2023 11:03:48 +0100 Subject: [PATCH 5/6] build: added some analysis options --- packages/at_auth/analysis_options.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/at_auth/analysis_options.yaml b/packages/at_auth/analysis_options.yaml index dee8927a..5b05276a 100644 --- a/packages/at_auth/analysis_options.yaml +++ b/packages/at_auth/analysis_options.yaml @@ -15,9 +15,13 @@ include: package:lints/recommended.yaml # Uncomment the following section to specify additional rules. -# linter: -# rules: -# - camel_case_types +linter: + rules: + camel_case_types : true + unnecessary_string_interpolations : true + await_only_futures : true + unawaited_futures: true + depend_on_referenced_packages : false # analyzer: # exclude: From 621e9037081a249a6185a5d0f972c60bcded52f5 Mon Sep 17 00:00:00 2001 From: gkc Date: Fri, 13 Oct 2023 11:04:40 +0100 Subject: [PATCH 6/6] fix: new analysis options picked up a missing `await` in at_auth_impl.dart. Added the `await` --- packages/at_auth/lib/src/at_auth_impl.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/at_auth/lib/src/at_auth_impl.dart b/packages/at_auth/lib/src/at_auth_impl.dart index f5e48247..513e7d12 100644 --- a/packages/at_auth/lib/src/at_auth_impl.dart +++ b/packages/at_auth/lib/src/at_auth_impl.dart @@ -133,7 +133,7 @@ class AtAuthImpl implements AtAuth { //3. Close connection to server try { - (atLookUp as AtLookupImpl).close(); + await (atLookUp as AtLookupImpl).close(); } on Exception catch (e) { _logger.severe('error while closing connection to server: $e'); }