Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Secure atKeys with pass-phrase #703

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 62 additions & 18 deletions packages/at_onboarding_cli/lib/src/cli/auth_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ 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/factory/service_factories.dart';
import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart';
import 'package:at_onboarding_cli/src/util/home_directory_util.dart';
import 'package:at_onboarding_cli/src/util/print_full_parser_usage.dart';
import 'package:at_utils/at_utils.dart';
import 'package:chalkdart/chalk.dart';
import 'package:duration/duration.dart';
import 'package:meta/meta.dart';

Expand Down Expand Up @@ -281,30 +284,68 @@ Future<int> status(ArgResults ar) async {
}

Future<AtClient> createAtClient(ArgResults ar) async {
final int maxConnectAttempts = 5;
String nameSpace = 'at_activate';
String atSign = AtUtils.fixAtSign(ar[AuthCliArgs.argNameAtSign]);
storageDir = standardAtClientStorageDir(
String? homeDir = HomeDirectoryUtil.homeDir;

storageDir = HomeDirectoryUtil.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();
// Get file path from the user. If null, search atKeys in the default filePath in user home directory.
String atKeysFilePathToUse = (ar[AuthCliArgs.argNameAtKeys] ??
'$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);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you extract the code below into its own file as a function which will return an AtClient given some args (similar to what we have in the noports repo here)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change in implemented in 5be8bf9

return cliBase.atClient;
AtOnboardingPreference atOnboardingPreference = AtOnboardingPreference()
..atKeysFilePath = atKeysFilePathToUse
..namespace = nameSpace
..rootDomain = ar[AuthCliArgs.argNameAtDirectoryFqdn]
..passPhrase = ar[AuthCliArgs.argNamePassPhrase]
..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 SecondaryServerConnectivityException(msg);
}
stderr.writeln(chalk.brightGreen('Connected'));
// Get the AtClient which the onboardingService just authenticated
return AtClientManager.getInstance().atClient;
}

/// When a cramSecret arg is not supplied, we first use the registrar API
Expand Down Expand Up @@ -996,7 +1037,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);
}
15 changes: 14 additions & 1 deletion packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<void> _persistKeysLocalSecondary() async {
//when authenticating keys need to be fetched from atKeys file
at_auth.AtAuthKeys atAuthKeys = _decryptAtKeysFile(
(await readAtKeysFile(atOnboardingPreference.atKeysFilePath)));
Future<void> _persistKeysLocalSecondary(at_auth.AtAuthKeys atAuthKeys) async {
//backup keys into local secondary
bool? response = await atClient
?.getLocalSecondary()
Expand Down Expand Up @@ -481,15 +487,16 @@ 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 &&
atOnboardingPreference.atKeysFilePath != null) {
logger.finer('Calling persist keys to local secondary');
await _initAtClient(atAuth!.atChops!,
enrollmentId: atAuthResponse.enrollmentId);
await _persistKeysLocalSecondary();
await _persistKeysLocalSecondary(atAuthResponse.atAuthKeys!);
}

return atAuthResponse.isSuccessful;
Expand All @@ -511,33 +518,6 @@ class AtOnboardingServiceImpl implements AtOnboardingService {
return jsonData;
}

///method to extract decryption key from atKeysData
///returns self_encryption_key
String _getDecryptionKey(Map<String, String>? jsonData) {
return jsonData![AuthKeyType.selfEncryptionKey]!;
}

at_auth.AtAuthKeys _decryptAtKeysFile(Map<String, String> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
61 changes: 61 additions & 0 deletions packages/at_onboarding_cli/lib/src/util/home_directory_util.dart
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -50,4 +53,62 @@ class HomeDirectoryUtil {
enrollmentId: enrollmentId),
'hive');
}

/// Generate a path like this:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like that we are duplicating this code, again. I think instead we should move these functions to at_utils from at_cli_commons. Rather than delay this PR further, please create another ticket to take care of this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created the following ticket: #720

/// $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,
));
}
}
}
11 changes: 6 additions & 5 deletions packages/at_onboarding_cli/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions packages/at_onboarding_cli/test/at_onboarding_cli_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions tests/at_onboarding_cli_functional_tests/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading