Skip to content

Commit

Permalink
Merge pull request #703 from atsign-foundation/604-support-password-p…
Browse files Browse the repository at this point in the history
…rotected-encryption-ofatkeys-files

feat: Secure atKeys with pass-phrase
  • Loading branch information
sitaram-kalluri authored Dec 2, 2024
2 parents 760af3a + 0bdb5e5 commit b80419a
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 111 deletions.
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

0 comments on commit b80419a

Please sign in to comment.