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 all 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
126 changes: 86 additions & 40 deletions packages/at_onboarding_cli/lib/src/cli/auth_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -148,7 +149,13 @@ Future<int> wrappedMain(List<String> 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
Expand All @@ -159,35 +166,86 @@ Future<int> wrappedMain(List<String> 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.
Expand All @@ -199,11 +257,23 @@ Future<int> wrappedMain(List<String> 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
Expand Down Expand Up @@ -280,33 +350,6 @@ Future<int> status(ArgResults ar) async {
return 0;
}

Future<AtClient> 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.
Expand Down Expand Up @@ -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);
}
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;
}
Loading