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..a834b55c 100644 --- a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart +++ b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart @@ -4,12 +4,13 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:at_auth/at_auth.dart'; -import 'package:at_cli_commons/at_cli_commons.dart'; +import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; import 'package:at_commons/at_builders.dart'; import 'package:at_lookup/at_lookup.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; +import 'package:at_onboarding_cli/src/util/create_at_client_cli.dart'; import 'package:at_onboarding_cli/src/util/print_full_parser_usage.dart'; import 'package:at_utils/at_utils.dart'; import 'package:duration/duration.dart'; @@ -148,7 +149,13 @@ Future wrappedMain(List arguments) async { // enrollment requests is used solely to defend against ddos attacks // where users are bombarded with spurious enrollment requests. await setSpp( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.otp: // generate a one-time-passcode for this atSign. This is a passcode @@ -159,35 +166,86 @@ Future wrappedMain(List arguments) async { // enrollment requests is used solely to defend against ddos attacks // where users are bombarded with spurious enrollment requests. await generateOtp( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.interactive: // Interactive session for various enrollment management activities: // - listing, approving, denying and revoking enrollments // - setting spp, generating otp, etc await interactive( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.list: - await list(commandArgResults, await createAtClient(commandArgResults)); + await list( + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.fetch: - await fetch(commandArgResults, await createAtClient(commandArgResults)); + await fetch( + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.approve: await approve( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.auto: await autoApprove( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.deny: - await deny(commandArgResults, await createAtClient(commandArgResults)); + await deny( + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.revoke: await revoke( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.enroll: // App which doesn't have auth keys and is not the first app. @@ -199,11 +257,23 @@ Future wrappedMain(List arguments) async { case AuthCliCommand.unrevoke: await unrevoke( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); case AuthCliCommand.delete: await deleteEnrollment( - commandArgResults, await createAtClient(commandArgResults)); + commandArgResults, + await createAtClient( + atSign: commandArgResults[AuthCliArgs.argNameAtSign], + atKeysFilePath: commandArgResults[AuthCliArgs.argNameAtKeys], + rootDomain: + commandArgResults[AuthCliArgs.argNameAtDirectoryFqdn], + passPhrase: commandArgResults[AuthCliArgs.argNamePassPhrase])); } } on ArgumentError catch (e) { stderr @@ -280,33 +350,6 @@ Future status(ArgResults ar) async { return 0; } -Future createAtClient(ArgResults ar) async { - String nameSpace = 'at_activate'; - String atSign = AtUtils.fixAtSign(ar[AuthCliArgs.argNameAtSign]); - storageDir = standardAtClientStorageDir( - atSign: atSign, - progName: nameSpace, - uniqueID: '${DateTime.now().millisecondsSinceEpoch}', - ); - - 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 - ); - - await cliBase.init(); - - return cliBase.atClient; -} - /// When a cramSecret arg is not supplied, we first use the registrar API /// to send an OTP to the user and then use that OTP to obtain the cram /// secret from the registrar. @@ -996,7 +1039,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..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 @@ -27,4 +27,7 @@ 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; } diff --git a/packages/at_onboarding_cli/lib/src/util/create_at_client_cli.dart b/packages/at_onboarding_cli/lib/src/util/create_at_client_cli.dart new file mode 100644 index 00000000..0cd58e71 --- /dev/null +++ b/packages/at_onboarding_cli/lib/src/util/create_at_client_cli.dart @@ -0,0 +1,82 @@ +import 'dart:io'; + +import 'package:at_client/at_client.dart'; +import 'package:at_onboarding_cli/src/cli/auth_cli.dart'; +import 'package:at_onboarding_cli/src/cli/auth_cli_args.dart'; +import 'package:at_onboarding_cli/src/factory/service_factories.dart'; +import 'package:at_onboarding_cli/src/onboard/at_onboarding_service.dart'; +import 'package:at_onboarding_cli/src/onboard/at_onboarding_service_impl.dart'; +import 'package:at_onboarding_cli/src/util/at_onboarding_preference.dart'; +import 'package:at_utils/at_utils.dart'; +import 'package:chalkdart/chalk.dart'; + +import 'home_directory_util.dart'; + +Future createAtClient( + {required String atSign, + String? atKeysFilePath, + String? rootDomain, + String? passPhrase}) async { + final int maxConnectAttempts = 5; + String nameSpace = 'at_activate'; + atSign = AtUtils.fixAtSign(atSign); + String? homeDir = HomeDirectoryUtil.homeDir; + + storageDir = HomeDirectoryUtil.standardAtClientStorageDir( + atSign: atSign, + progName: nameSpace, + uniqueID: '${DateTime.now().millisecondsSinceEpoch}', + ); + + // Get file path from the user. If null, search atKeys in the default filePath in user home directory. + String atKeysFilePathToUse = + (atKeysFilePath ?? '$homeDir/.atsign/keys/${atSign}_key.atKeys') + .replaceAll('/', Platform.pathSeparator); + String? localStoragePathToUse = storageDir?.path; + String? commitLogStoragePathToUse = + ('${storageDir?.path}/commit').replaceAll('/', Platform.pathSeparator); + String downloadPathToUse = ('$homeDir!/.atsign/downloads/$atSign/$nameSpace') + .replaceAll('/', Platform.pathSeparator); + + AtOnboardingPreference atOnboardingPreference = AtOnboardingPreference() + ..atKeysFilePath = atKeysFilePathToUse + ..namespace = nameSpace + ..rootDomain = rootDomain ?? AuthCliArgs.defaultAtDirectoryFqdn + ..passPhrase = passPhrase + ..hiveStoragePath = localStoragePathToUse + ..commitLogPath = commitLogStoragePathToUse + ..downloadPath = downloadPathToUse; + + AtOnboardingService atOnboardingService = AtOnboardingServiceImpl( + atSign, atOnboardingPreference, + atServiceFactory: ServiceFactoryWithNoOpSyncService()); + + bool authenticated = false; + Duration retryDuration = Duration(seconds: 3); + int attempts = 0; + while (!authenticated && attempts < maxConnectAttempts) { + try { + stderr.write(chalk.brightBlue('\r\x1b[KConnecting ... ')); + attempts++; + await Future.delayed(Duration( + milliseconds: + 1000)); // Pause just long enough for the retry to be visible + authenticated = await atOnboardingService.authenticate(); + } catch (exception) { + stderr.write(chalk.brightRed( + '$exception. Will retry in ${retryDuration.inSeconds} seconds')); + } + if (!authenticated) { + await Future.delayed(retryDuration); + } + } + if (!authenticated) { + stderr.writeln(); + var msg = 'Failed to connect after $attempts attempts'; + stderr.writeln(chalk.brightRed(msg)); + throw UnAuthenticatedException(msg); + } + stderr.writeln(chalk.brightGreen('Connected')); + // Get the AtClient which the onboardingService just authenticated + return AtClientManager.getInstance().atClient; +} diff --git a/packages/at_onboarding_cli/lib/src/util/home_directory_util.dart b/packages/at_onboarding_cli/lib/src/util/home_directory_util.dart index 70add5e5..dd681c87 100644 --- a/packages/at_onboarding_cli/lib/src/util/home_directory_util.dart +++ b/packages/at_onboarding_cli/lib/src/util/home_directory_util.dart @@ -1,8 +1,11 @@ import 'dart:io'; + import 'package:at_client/at_client.dart'; import 'package:path/path.dart' as path; class HomeDirectoryUtil { + static const String defaultPathUniqueID = 'singleton'; + static final homeDir = getHomeDirectory(); static String? getHomeDirectory() { @@ -50,4 +53,62 @@ class HomeDirectoryUtil { enrollmentId: enrollmentId), 'hive'); } + + /// Generate a path like this: + /// $baseDir/.atsign/storage/$atSign/$progName/$uniqueID + /// (normalized to use platform-specific path separator) + static String standardAtClientStoragePath({ + required String baseDir, + required String atSign, + required String progName, // e.g. npt, sshnp, sshnpd, srvd etc + String uniqueID = defaultPathUniqueID, + }) { + return path.normalize('$baseDir' + '/.atsign' + '/storage' + '/$atSign' + '/$progName' + '/$uniqueID' + .replaceAll('/', Platform.pathSeparator)); + } + + Directory standardWindowsAtClientStorageDir({ + required String atSign, + required String progName, // e.g. npt, sshnp, sshnpd, srvd etc + required String uniqueID, + }) { + return Directory(standardAtClientStoragePath( + baseDir: Platform.environment['TEMP']!, + atSign: atSign, + progName: progName, + uniqueID: uniqueID, + )); + } + + /// Generate a path like this: + /// $baseDir/.atsign/storage/$atSign/$progName/$uniqueID + /// (normalized to use platform-specific path separator) + /// where baseDir is either + /// - for Windows: `Platform.environment['TEMP']` + /// - for others: [getHomeDirectory] + static Directory standardAtClientStorageDir({ + required String atSign, + required String progName, // e.g. npt, sshnp, sshnpd, srvd etc + required String uniqueID, + }) { + if (Platform.isWindows) { + return standardAtClientStorageDir( + atSign: atSign, + progName: progName, + uniqueID: uniqueID, + ); + } else { + return Directory(standardAtClientStoragePath( + baseDir: getHomeDirectory()!, + atSign: atSign, + progName: progName, + uniqueID: uniqueID, + )); + } + } } diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index bfd75ae3..08e6a834 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -21,16 +21,17 @@ dependencies: meta: ^1.14.0 path: ^1.9.0 zxing2: ^0.2.0 - at_auth: ^2.0.7 - at_chops: ^2.0.1 - at_client: ^3.2.2 - at_commons: ^5.0.0 + at_auth: ^2.0.8 + at_chops: ^2.2.0 + at_client: ^3.3.0 + at_commons: ^5.0.2 at_lookup: ^3.0.49 at_server_status: ^1.0.5 at_utils: ^3.0.19 - at_cli_commons: ^1.2.0 at_persistence_secondary_server: ^3.0.64 duration: ^4.0.3 + crypto: ^3.0.5 + chalkdart: ^2.0.9 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/pubspec.yaml b/tests/at_onboarding_cli_functional_tests/pubspec.yaml index ed30a139..1e6171f4 100644 --- a/tests/at_onboarding_cli_functional_tests/pubspec.yaml +++ b/tests/at_onboarding_cli_functional_tests/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: at_lookup: ^3.0.49 at_client: ^3.2.2 at_commons: ^5.0.0 + at_demo_data: ^1.2.0 dependency_overrides: at_auth: @@ -18,6 +19,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 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..e0143ec0 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,7 +1,9 @@ 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_demo_data/at_demo_data.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; import 'package:at_onboarding_cli/src/cli/auth_cli.dart' as auth_cli; import 'package:at_utils/at_utils.dart'; @@ -11,6 +13,33 @@ 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 = cramKeyMap[atSign]); + + 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); }); }