Skip to content

Commit

Permalink
Merge pull request #1348 from atsign-foundation/at_client_offline_access
Browse files Browse the repository at this point in the history
fix: Enable at_client to initialize in offline when network is down
  • Loading branch information
sitaram-kalluri authored Jul 22, 2024
2 parents 099331c + 94ee3a8 commit 614028e
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 29 deletions.
6 changes: 4 additions & 2 deletions packages/at_client_mobile/lib/src/at_client_mobile_base.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:at_client_mobile/at_client_mobile.dart';
import 'package:at_client_mobile/src/auth/at_auth_service_impl.dart';
import 'package:at_lookup/at_lookup.dart';

/// The Base class to expose the AtClientMobile services.
class AtClientMobile {
Expand All @@ -9,7 +10,8 @@ class AtClientMobile {
///
/// AtAuthService authService = AtClientMobile.authService(_atsign!, _atClientPreference);
static AtAuthService authService(
String atSign, AtClientPreference atClientPreference) {
return AtAuthServiceImpl(atSign, atClientPreference);
String atSign, AtClientPreference atClientPreference,
{AtLookUp? atLookUp}) {
return AtAuthServiceImpl(atSign, atClientPreference, atLookUp: atLookUp);
}
}
65 changes: 40 additions & 25 deletions packages/at_client_mobile/lib/src/auth/at_auth_service_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@ class AtAuthServiceImpl implements AtAuthService {

AtServiceFactory? atServiceFactory;
AtClient? _atClient;

@visibleForTesting
AtLookUp? atLookUp;

AtLookUp? _atLookUp;
String _atSign;
final AtClientPreference _atClientPreference;
late AtAuth _atAuth;

@visibleForTesting
KeyChainManager keyChainManager = KeyChainManager.getInstance();

late AtAuth _atAuth;

@visibleForTesting
late AtEnrollmentBase atEnrollmentBase;

Expand All @@ -48,13 +44,15 @@ class AtAuthServiceImpl implements AtAuthService {
/// ```dart
/// AtAuthService authService = AtClientMobile.authService(_atsign!, _atClientPreference);
/// ```
AtAuthServiceImpl(this._atSign, this._atClientPreference) {
AtAuthServiceImpl(this._atSign, this._atClientPreference,
{AtLookUp? atLookUp})
: _atLookUp = atLookUp {
// If the '@' symbol is omitted, it leads to an incorrect format for the AtKey when retrieving the
// encrypted defaultEncryptionPrivateKey and encrypted defaultSelfEncryptionKey.
if (!_atSign.startsWith('@')) {
_atSign = '@$_atSign';
}
_atAuth = atAuthBase.atAuth();
_atAuth = atAuthBase.atAuth(atLookUp: _atLookUp);
atEnrollmentBase = atAuthBase.atEnrollment(_atSign);
}

Expand All @@ -75,7 +73,24 @@ class AtAuthServiceImpl implements AtAuthService {
atAuthRequest.atAuthKeys = await _fetchKeysFromKeychainManager();
}
// Invoke authenticate method in AtAuth package.
AtAuthResponse atAuthResponse = await _atAuth.authenticate(atAuthRequest);
AtAuthResponse atAuthResponse = AtAuthResponse(_atSign);
try {
atAuthResponse = await _atAuth.authenticate(atAuthRequest);
} on AtAuthenticationException {
// AtAuthenticationException could be because of authentication failure or due to network failure.
// If due to network failure, initialize atClient for offline access. To initialize atClient in offline,
// check if the atSign is already onboarded. If already onboarded, initialize atClient for offline usage.
if (await isOnboarded(_atSign)) {
// Initialize atClient for offline access.
_logger.info(
'Network connectivity not available. Initializing at_client for offline usage');
await _init(_atAuth.atChops!, enrollmentId: atAuthRequest.enrollmentId);
return atAuthResponse
..isSuccessful = true
..atAuthKeys = atAuthRequest.atAuthKeys
..enrollmentId = atAuthRequest.enrollmentId;
}
}
// If authentication is failed, return the atAuthResponse. Do nothing.
if (atAuthResponse.isSuccessful == false) {
return atAuthResponse;
Expand Down Expand Up @@ -207,7 +222,7 @@ class AtAuthServiceImpl implements AtAuthService {

Future<void> _init(AtChops atChops, {String? enrollmentId}) async {
await _initAtClient(atChops, enrollmentId: enrollmentId);
atLookUp!.atChops = atChops;
_atLookUp!.atChops = atChops;
_atClient!.atChops = atChops;
}

Expand All @@ -219,10 +234,10 @@ class AtAuthServiceImpl implements AtAuthService {
serviceFactory: atServiceFactory,
enrollmentId: enrollmentId);
// ??= to support mocking
atLookUp ??= atClientManager.atClient.getRemoteSecondary()?.atLookUp;
atLookUp?.enrollmentId = enrollmentId;
atLookUp?.signingAlgoType = _atClientPreference.signingAlgoType;
atLookUp?.hashingAlgoType = _atClientPreference.hashingAlgoType;
_atLookUp ??= atClientManager.atClient.getRemoteSecondary()?.atLookUp;
_atLookUp?.enrollmentId = enrollmentId;
_atLookUp?.signingAlgoType = _atClientPreference.signingAlgoType;
_atLookUp?.hashingAlgoType = _atClientPreference.hashingAlgoType;
_atClient ??= atClientManager.atClient;
}

Expand All @@ -240,11 +255,11 @@ class AtAuthServiceImpl implements AtAuthService {
throw AtEnrollmentException(
'Cannot submit new enrollment request until the pending enrollment request is fulfilled');
}
atLookUp ??= AtLookupImpl(
_atLookUp ??= AtLookupImpl(
_atSign, _atClientPreference.rootDomain, _atClientPreference.rootPort);
AtEnrollmentResponse atEnrollmentResponse =
await atEnrollmentBase.submit(enrollmentRequest, atLookUp!);
await atLookUp?.close();
await atEnrollmentBase.submit(enrollmentRequest, _atLookUp!);
await _atLookUp?.close();
EnrollmentInfo enrollmentInfo = EnrollmentInfo(
atEnrollmentResponse.enrollmentId,
atEnrollmentResponse.atAuthKeys!,
Expand Down Expand Up @@ -356,7 +371,7 @@ class AtAuthServiceImpl implements AtAuthService {
/// Returns UnAuthenticatedException if the enrollment is in pending state or denied.
Future<bool?> _performAPKAMAuthentication(
EnrollmentInfo enrollmentInfo) async {
atLookUp ??= AtLookupImpl(
_atLookUp ??= AtLookupImpl(
_atSign, _atClientPreference.rootDomain, _atClientPreference.rootPort);
// Create the AtChops instance with the new APKAM keys to verify if enrollment
// is approved.
Expand All @@ -367,9 +382,9 @@ class AtAuthServiceImpl implements AtAuthService {
enrollmentInfo.atAuthKeys.apkamPrivateKey!));
atChopsKeys.apkamSymmetricKey =
AESKey(enrollmentInfo.atAuthKeys.apkamSymmetricKey!);
atLookUp?.atChops = AtChopsImpl(atChopsKeys);
_atLookUp?.atChops = AtChopsImpl(atChopsKeys);

return await atLookUp?.pkamAuthenticate(
return await _atLookUp?.pkamAuthenticate(
enrollmentId: enrollmentInfo.enrollmentId);
}

Expand All @@ -383,10 +398,10 @@ class AtAuthServiceImpl implements AtAuthService {
// from the secondary server.
enrollmentInfo.atAuthKeys.defaultEncryptionPrivateKey =
await _getDefaultEncryptionPrivateKey(
enrollmentInfo.enrollmentId, atLookUp!.atChops!);
enrollmentInfo.enrollmentId, _atLookUp!.atChops!);
enrollmentInfo.atAuthKeys.defaultSelfEncryptionKey =
await _getDefaultSelfEncryptionKey(
enrollmentInfo.enrollmentId, atLookUp!.atChops!);
enrollmentInfo.enrollmentId, _atLookUp!.atChops!);
// Store the auth keys into keychain manager for subsequent authentications
await _storeToKeyChainManager(_atSign, enrollmentInfo.atAuthKeys);
AtChops atChops = _buildAtChops(enrollmentInfo);
Expand All @@ -398,7 +413,7 @@ class AtAuthServiceImpl implements AtAuthService {
_logger.info(
'Enrollment Id: ${enrollmentInfo.atAuthKeys.enrollmentId} is approved and authentication keys are stored in the keychain');
_outcomes[enrollmentInfo.enrollmentId]?.complete(EnrollmentStatus.approved);
atLookUp?.close();
_atLookUp?.close();
}

/// When PKAM authentication is failed, return UnAuthenticatedException.
Expand Down Expand Up @@ -436,7 +451,7 @@ class AtAuthServiceImpl implements AtAuthService {
String encryptionPrivateKeyFromServer;
try {
var getPrivateKeyResult =
await atLookUp?.executeCommand('$privateKeyCommand\n', auth: true);
await _atLookUp?.executeCommand('$privateKeyCommand\n', auth: true);
if (getPrivateKeyResult == null || getPrivateKeyResult.isEmpty) {
throw AtEnrollmentException('$privateKeyCommand returned null/empty');
}
Expand All @@ -462,7 +477,7 @@ class AtAuthServiceImpl implements AtAuthService {
'keys:get:keyName:$enrollmentIdFromServer.${AtConstants.defaultSelfEncryptionKey}.__manage$_atSign\n';
String selfEncryptionKeyFromServer;
try {
String? encryptedSelfEncryptionKey = await atLookUp
String? encryptedSelfEncryptionKey = await _atLookUp
?.executeCommand('$selfEncryptionKeyCommand\n', auth: true);
if (encryptedSelfEncryptionKey == null ||
encryptedSelfEncryptionKey.isEmpty) {
Expand Down
148 changes: 146 additions & 2 deletions packages/at_client_mobile/test/at_auth_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import 'dart:convert';
import 'package:at_auth/at_auth.dart';
import 'package:at_chops/at_chops.dart';
import 'package:at_client_mobile/at_client_mobile.dart';
import 'package:at_client_mobile/src/atsign_key.dart';
import 'package:at_client_mobile/src/auth/at_auth_service_impl.dart';
import 'package:at_commons/at_builders.dart';
import 'package:at_lookup/at_lookup.dart';
import 'package:biometric_storage/biometric_storage.dart';
import 'package:crypton/crypton.dart';
import 'package:mocktail/mocktail.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:test/test.dart';

import 'at_client_service_test.dart';

class MockBiometricStorage extends Mock implements BiometricStorage {}

class MockEnrollmentBiometricStorageFile extends Mock
Expand Down Expand Up @@ -69,14 +73,14 @@ void main() {
late MockAtLookUp mockAtLookUp;

setUp(() {
authServiceImpl = AtAuthServiceImpl(atSign, atClientPreference);
mockBiometricStorageEnrollmentFile = MockEnrollmentBiometricStorageFile();
mockBiometricStorage = MockBiometricStorage();
mockBiometricStorageKeychainFile = MockKeychainBiometricStorageFile();
mockAtLookUp = MockAtLookUp();

authServiceImpl =
AtAuthServiceImpl(atSign, atClientPreference, atLookUp: mockAtLookUp);
authServiceImpl.keyChainManager.biometricStorage = mockBiometricStorage;
authServiceImpl.atLookUp = mockAtLookUp;
});

test('A test to verify submission of enrollment', () async {
Expand Down Expand Up @@ -464,6 +468,146 @@ void main() {
tearDown(() => tearDownMethod(mockBiometricStorageEnrollmentFile,
mockAtLookUp, mockBiometricStorage));
});

group('A group of tests related to authenticate an atSign', () {
String atSign = '@alice';
AtClientPreference atClientPreference = AtClientPreference()
..namespace = 'me';

test(
'A test to verify AtClient initializes successfully in offline mode upon network failure when keychain manager contains keys',
() async {
KeyChainManager mockKeyChainManager = MockKeyChainManager();
RSAKeypair pkamKeyPair = KeyChainManager.getInstance().generateKeyPair();
RSAKeypair encryptionKeyPair =
KeyChainManager.getInstance().generateKeyPair();
String selfEncryptionKey = KeyChainManager.getInstance().generateAESKey();
AtAuthService atAuthService =
AtClientMobile.authService(atSign, atClientPreference);
(atAuthService as AtAuthServiceImpl).keyChainManager =
mockKeyChainManager;

AtsignKey atsignKey = AtsignKey(
atSign: atSign,
encryptionPublicKey: encryptionKeyPair.publicKey.toString());

// Mock object to return keys from keychain manager
when(() => mockKeyChainManager.readAtsign(name: atSign))
.thenAnswer((_) => Future.value(atsignKey));

AtAuthRequest atAuthRequest = AtAuthRequest(atSign);
atAuthRequest.atAuthKeys = AtAuthKeys()
..apkamPrivateKey = pkamKeyPair.privateKey.toString()
..apkamPublicKey = pkamKeyPair.publicKey.toString()
..defaultEncryptionPublicKey = encryptionKeyPair.publicKey.toString()
..defaultEncryptionPrivateKey = encryptionKeyPair.privateKey.toString()
..defaultSelfEncryptionKey = selfEncryptionKey
..enrollmentId = '123';

AtAuthResponse atAuthResponse =
await atAuthService.authenticate(atAuthRequest);

expect(atAuthResponse.isSuccessful, true);
expect(atAuthResponse.atSign, atSign);
expect(atAuthResponse.atAuthKeys?.apkamPublicKey,
pkamKeyPair.publicKey.toString());
expect(atAuthResponse.atAuthKeys?.apkamPrivateKey,
pkamKeyPair.privateKey.toString());
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPrivateKey,
encryptionKeyPair.privateKey.toString());
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPublicKey,
encryptionKeyPair.publicKey.toString());
expect(atAuthResponse.atAuthKeys?.defaultSelfEncryptionKey,
selfEncryptionKey);
});

test(
'A test to verify atClient initialization fails when network is offline and keychain manager does not have keys',
() async {
KeyChainManager mockKeyChainManager = MockKeyChainManager();
RSAKeypair pkamKeyPair = KeyChainManager.getInstance().generateKeyPair();
RSAKeypair encryptionKeyPair =
KeyChainManager.getInstance().generateKeyPair();
AtAuthService atAuthService =
AtClientMobile.authService(atSign, atClientPreference);
(atAuthService as AtAuthServiceImpl).keyChainManager =
mockKeyChainManager;

AtsignKey atsignKey = AtsignKey(atSign: atSign, encryptionPublicKey: '');

// Mock object to return keys from keychain manager
when(() => mockKeyChainManager.readAtsign(name: atSign))
.thenAnswer((_) => Future.value(atsignKey));

AtAuthRequest atAuthRequest = AtAuthRequest(atSign);
atAuthRequest.atAuthKeys = AtAuthKeys()
..apkamPrivateKey = pkamKeyPair.privateKey.toString()
..apkamPublicKey = pkamKeyPair.publicKey.toString()
..defaultEncryptionPublicKey = encryptionKeyPair.publicKey.toString()
..defaultEncryptionPrivateKey = encryptionKeyPair.privateKey.toString()
..defaultSelfEncryptionKey =
KeyChainManager.getInstance().generateAESKey();

AtAuthResponse atAuthResponse =
await atAuthService.authenticate(atAuthRequest);

expect(atAuthResponse.isSuccessful, false);
});

test(
'A test to verify authentication is successful when pkamAuthentication returns true',
() async {
KeyChainManager mockKeyChainManager = MockKeyChainManager();
AtLookUp mockAtLookup = MockAtLookUp();

RSAKeypair pkamKeyPair = KeyChainManager.getInstance().generateKeyPair();
RSAKeypair encryptionKeyPair =
KeyChainManager.getInstance().generateKeyPair();
String selfEncryptionKey = KeyChainManager.getInstance().generateAESKey();
AtAuthService atAuthService = AtClientMobile.authService(
atSign, atClientPreference,
atLookUp: mockAtLookup);
(atAuthService as AtAuthServiceImpl).keyChainManager =
mockKeyChainManager;

AtsignKey atsignKey = AtsignKey(
atSign: atSign,
encryptionPublicKey: encryptionKeyPair.publicKey.toString());

// Mock object to return keys from keychain manager
when(() => mockKeyChainManager.readAtsign(name: atSign))
.thenAnswer((_) => Future.value(atsignKey));

when(() => mockAtLookup.pkamAuthenticate(enrollmentId: '123'))
.thenAnswer((_) => Future.value(true));

AtAuthRequest atAuthRequest = AtAuthRequest(atSign);
atAuthRequest.atAuthKeys = AtAuthKeys()
..apkamPrivateKey = pkamKeyPair.privateKey.toString()
..apkamPublicKey = pkamKeyPair.publicKey.toString()
..defaultEncryptionPublicKey = encryptionKeyPair.publicKey.toString()
..defaultEncryptionPrivateKey = encryptionKeyPair.privateKey.toString()
..defaultSelfEncryptionKey = selfEncryptionKey
..enrollmentId = '123';

AtAuthResponse atAuthResponse =
await atAuthService.authenticate(atAuthRequest);

expect(atAuthResponse.isSuccessful, true);
expect(atAuthResponse.atSign, atSign);
expect(atAuthResponse.atAuthKeys?.enrollmentId, '123');
expect(atAuthResponse.atAuthKeys?.apkamPublicKey,
pkamKeyPair.publicKey.toString());
expect(atAuthResponse.atAuthKeys?.apkamPrivateKey,
pkamKeyPair.privateKey.toString());
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPrivateKey,
encryptionKeyPair.privateKey.toString());
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPublicKey,
encryptionKeyPair.publicKey.toString());
expect(atAuthResponse.atAuthKeys?.defaultSelfEncryptionKey,
selfEncryptionKey);
});
});
}

void tearDownMethod(MockEnrollmentBiometricStorageFile mockBiometricStorageFile,
Expand Down

0 comments on commit 614028e

Please sign in to comment.