Skip to content

Commit 614028e

Browse files
Merge pull request #1348 from atsign-foundation/at_client_offline_access
fix: Enable at_client to initialize in offline when network is down
2 parents 099331c + 94ee3a8 commit 614028e

File tree

3 files changed

+190
-29
lines changed

3 files changed

+190
-29
lines changed
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:at_client_mobile/at_client_mobile.dart';
22
import 'package:at_client_mobile/src/auth/at_auth_service_impl.dart';
3+
import 'package:at_lookup/at_lookup.dart';
34

45
/// The Base class to expose the AtClientMobile services.
56
class AtClientMobile {
@@ -9,7 +10,8 @@ class AtClientMobile {
910
///
1011
/// AtAuthService authService = AtClientMobile.authService(_atsign!, _atClientPreference);
1112
static AtAuthService authService(
12-
String atSign, AtClientPreference atClientPreference) {
13-
return AtAuthServiceImpl(atSign, atClientPreference);
13+
String atSign, AtClientPreference atClientPreference,
14+
{AtLookUp? atLookUp}) {
15+
return AtAuthServiceImpl(atSign, atClientPreference, atLookUp: atLookUp);
1416
}
1517
}

packages/at_client_mobile/lib/src/auth/at_auth_service_impl.dart

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@ class AtAuthServiceImpl implements AtAuthService {
1616

1717
AtServiceFactory? atServiceFactory;
1818
AtClient? _atClient;
19-
20-
@visibleForTesting
21-
AtLookUp? atLookUp;
22-
19+
AtLookUp? _atLookUp;
2320
String _atSign;
2421
final AtClientPreference _atClientPreference;
22+
late AtAuth _atAuth;
2523

2624
@visibleForTesting
2725
KeyChainManager keyChainManager = KeyChainManager.getInstance();
2826

29-
late AtAuth _atAuth;
30-
3127
@visibleForTesting
3228
late AtEnrollmentBase atEnrollmentBase;
3329

@@ -48,13 +44,15 @@ class AtAuthServiceImpl implements AtAuthService {
4844
/// ```dart
4945
/// AtAuthService authService = AtClientMobile.authService(_atsign!, _atClientPreference);
5046
/// ```
51-
AtAuthServiceImpl(this._atSign, this._atClientPreference) {
47+
AtAuthServiceImpl(this._atSign, this._atClientPreference,
48+
{AtLookUp? atLookUp})
49+
: _atLookUp = atLookUp {
5250
// If the '@' symbol is omitted, it leads to an incorrect format for the AtKey when retrieving the
5351
// encrypted defaultEncryptionPrivateKey and encrypted defaultSelfEncryptionKey.
5452
if (!_atSign.startsWith('@')) {
5553
_atSign = '@$_atSign';
5654
}
57-
_atAuth = atAuthBase.atAuth();
55+
_atAuth = atAuthBase.atAuth(atLookUp: _atLookUp);
5856
atEnrollmentBase = atAuthBase.atEnrollment(_atSign);
5957
}
6058

@@ -75,7 +73,24 @@ class AtAuthServiceImpl implements AtAuthService {
7573
atAuthRequest.atAuthKeys = await _fetchKeysFromKeychainManager();
7674
}
7775
// Invoke authenticate method in AtAuth package.
78-
AtAuthResponse atAuthResponse = await _atAuth.authenticate(atAuthRequest);
76+
AtAuthResponse atAuthResponse = AtAuthResponse(_atSign);
77+
try {
78+
atAuthResponse = await _atAuth.authenticate(atAuthRequest);
79+
} on AtAuthenticationException {
80+
// AtAuthenticationException could be because of authentication failure or due to network failure.
81+
// If due to network failure, initialize atClient for offline access. To initialize atClient in offline,
82+
// check if the atSign is already onboarded. If already onboarded, initialize atClient for offline usage.
83+
if (await isOnboarded(_atSign)) {
84+
// Initialize atClient for offline access.
85+
_logger.info(
86+
'Network connectivity not available. Initializing at_client for offline usage');
87+
await _init(_atAuth.atChops!, enrollmentId: atAuthRequest.enrollmentId);
88+
return atAuthResponse
89+
..isSuccessful = true
90+
..atAuthKeys = atAuthRequest.atAuthKeys
91+
..enrollmentId = atAuthRequest.enrollmentId;
92+
}
93+
}
7994
// If authentication is failed, return the atAuthResponse. Do nothing.
8095
if (atAuthResponse.isSuccessful == false) {
8196
return atAuthResponse;
@@ -207,7 +222,7 @@ class AtAuthServiceImpl implements AtAuthService {
207222

208223
Future<void> _init(AtChops atChops, {String? enrollmentId}) async {
209224
await _initAtClient(atChops, enrollmentId: enrollmentId);
210-
atLookUp!.atChops = atChops;
225+
_atLookUp!.atChops = atChops;
211226
_atClient!.atChops = atChops;
212227
}
213228

@@ -219,10 +234,10 @@ class AtAuthServiceImpl implements AtAuthService {
219234
serviceFactory: atServiceFactory,
220235
enrollmentId: enrollmentId);
221236
// ??= to support mocking
222-
atLookUp ??= atClientManager.atClient.getRemoteSecondary()?.atLookUp;
223-
atLookUp?.enrollmentId = enrollmentId;
224-
atLookUp?.signingAlgoType = _atClientPreference.signingAlgoType;
225-
atLookUp?.hashingAlgoType = _atClientPreference.hashingAlgoType;
237+
_atLookUp ??= atClientManager.atClient.getRemoteSecondary()?.atLookUp;
238+
_atLookUp?.enrollmentId = enrollmentId;
239+
_atLookUp?.signingAlgoType = _atClientPreference.signingAlgoType;
240+
_atLookUp?.hashingAlgoType = _atClientPreference.hashingAlgoType;
226241
_atClient ??= atClientManager.atClient;
227242
}
228243

@@ -240,11 +255,11 @@ class AtAuthServiceImpl implements AtAuthService {
240255
throw AtEnrollmentException(
241256
'Cannot submit new enrollment request until the pending enrollment request is fulfilled');
242257
}
243-
atLookUp ??= AtLookupImpl(
258+
_atLookUp ??= AtLookupImpl(
244259
_atSign, _atClientPreference.rootDomain, _atClientPreference.rootPort);
245260
AtEnrollmentResponse atEnrollmentResponse =
246-
await atEnrollmentBase.submit(enrollmentRequest, atLookUp!);
247-
await atLookUp?.close();
261+
await atEnrollmentBase.submit(enrollmentRequest, _atLookUp!);
262+
await _atLookUp?.close();
248263
EnrollmentInfo enrollmentInfo = EnrollmentInfo(
249264
atEnrollmentResponse.enrollmentId,
250265
atEnrollmentResponse.atAuthKeys!,
@@ -356,7 +371,7 @@ class AtAuthServiceImpl implements AtAuthService {
356371
/// Returns UnAuthenticatedException if the enrollment is in pending state or denied.
357372
Future<bool?> _performAPKAMAuthentication(
358373
EnrollmentInfo enrollmentInfo) async {
359-
atLookUp ??= AtLookupImpl(
374+
_atLookUp ??= AtLookupImpl(
360375
_atSign, _atClientPreference.rootDomain, _atClientPreference.rootPort);
361376
// Create the AtChops instance with the new APKAM keys to verify if enrollment
362377
// is approved.
@@ -367,9 +382,9 @@ class AtAuthServiceImpl implements AtAuthService {
367382
enrollmentInfo.atAuthKeys.apkamPrivateKey!));
368383
atChopsKeys.apkamSymmetricKey =
369384
AESKey(enrollmentInfo.atAuthKeys.apkamSymmetricKey!);
370-
atLookUp?.atChops = AtChopsImpl(atChopsKeys);
385+
_atLookUp?.atChops = AtChopsImpl(atChopsKeys);
371386

372-
return await atLookUp?.pkamAuthenticate(
387+
return await _atLookUp?.pkamAuthenticate(
373388
enrollmentId: enrollmentInfo.enrollmentId);
374389
}
375390

@@ -383,10 +398,10 @@ class AtAuthServiceImpl implements AtAuthService {
383398
// from the secondary server.
384399
enrollmentInfo.atAuthKeys.defaultEncryptionPrivateKey =
385400
await _getDefaultEncryptionPrivateKey(
386-
enrollmentInfo.enrollmentId, atLookUp!.atChops!);
401+
enrollmentInfo.enrollmentId, _atLookUp!.atChops!);
387402
enrollmentInfo.atAuthKeys.defaultSelfEncryptionKey =
388403
await _getDefaultSelfEncryptionKey(
389-
enrollmentInfo.enrollmentId, atLookUp!.atChops!);
404+
enrollmentInfo.enrollmentId, _atLookUp!.atChops!);
390405
// Store the auth keys into keychain manager for subsequent authentications
391406
await _storeToKeyChainManager(_atSign, enrollmentInfo.atAuthKeys);
392407
AtChops atChops = _buildAtChops(enrollmentInfo);
@@ -398,7 +413,7 @@ class AtAuthServiceImpl implements AtAuthService {
398413
_logger.info(
399414
'Enrollment Id: ${enrollmentInfo.atAuthKeys.enrollmentId} is approved and authentication keys are stored in the keychain');
400415
_outcomes[enrollmentInfo.enrollmentId]?.complete(EnrollmentStatus.approved);
401-
atLookUp?.close();
416+
_atLookUp?.close();
402417
}
403418

404419
/// When PKAM authentication is failed, return UnAuthenticatedException.
@@ -436,7 +451,7 @@ class AtAuthServiceImpl implements AtAuthService {
436451
String encryptionPrivateKeyFromServer;
437452
try {
438453
var getPrivateKeyResult =
439-
await atLookUp?.executeCommand('$privateKeyCommand\n', auth: true);
454+
await _atLookUp?.executeCommand('$privateKeyCommand\n', auth: true);
440455
if (getPrivateKeyResult == null || getPrivateKeyResult.isEmpty) {
441456
throw AtEnrollmentException('$privateKeyCommand returned null/empty');
442457
}
@@ -462,7 +477,7 @@ class AtAuthServiceImpl implements AtAuthService {
462477
'keys:get:keyName:$enrollmentIdFromServer.${AtConstants.defaultSelfEncryptionKey}.__manage$_atSign\n';
463478
String selfEncryptionKeyFromServer;
464479
try {
465-
String? encryptedSelfEncryptionKey = await atLookUp
480+
String? encryptedSelfEncryptionKey = await _atLookUp
466481
?.executeCommand('$selfEncryptionKeyCommand\n', auth: true);
467482
if (encryptedSelfEncryptionKey == null ||
468483
encryptedSelfEncryptionKey.isEmpty) {

packages/at_client_mobile/test/at_auth_service_test.dart

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import 'dart:convert';
44
import 'package:at_auth/at_auth.dart';
55
import 'package:at_chops/at_chops.dart';
66
import 'package:at_client_mobile/at_client_mobile.dart';
7+
import 'package:at_client_mobile/src/atsign_key.dart';
78
import 'package:at_client_mobile/src/auth/at_auth_service_impl.dart';
89
import 'package:at_commons/at_builders.dart';
910
import 'package:at_lookup/at_lookup.dart';
1011
import 'package:biometric_storage/biometric_storage.dart';
12+
import 'package:crypton/crypton.dart';
1113
import 'package:mocktail/mocktail.dart';
1214
import 'package:package_info_plus/package_info_plus.dart';
1315
import 'package:test/test.dart';
1416

17+
import 'at_client_service_test.dart';
18+
1519
class MockBiometricStorage extends Mock implements BiometricStorage {}
1620

1721
class MockEnrollmentBiometricStorageFile extends Mock
@@ -69,14 +73,14 @@ void main() {
6973
late MockAtLookUp mockAtLookUp;
7074

7175
setUp(() {
72-
authServiceImpl = AtAuthServiceImpl(atSign, atClientPreference);
7376
mockBiometricStorageEnrollmentFile = MockEnrollmentBiometricStorageFile();
7477
mockBiometricStorage = MockBiometricStorage();
7578
mockBiometricStorageKeychainFile = MockKeychainBiometricStorageFile();
7679
mockAtLookUp = MockAtLookUp();
7780

81+
authServiceImpl =
82+
AtAuthServiceImpl(atSign, atClientPreference, atLookUp: mockAtLookUp);
7883
authServiceImpl.keyChainManager.biometricStorage = mockBiometricStorage;
79-
authServiceImpl.atLookUp = mockAtLookUp;
8084
});
8185

8286
test('A test to verify submission of enrollment', () async {
@@ -464,6 +468,146 @@ void main() {
464468
tearDown(() => tearDownMethod(mockBiometricStorageEnrollmentFile,
465469
mockAtLookUp, mockBiometricStorage));
466470
});
471+
472+
group('A group of tests related to authenticate an atSign', () {
473+
String atSign = '@alice';
474+
AtClientPreference atClientPreference = AtClientPreference()
475+
..namespace = 'me';
476+
477+
test(
478+
'A test to verify AtClient initializes successfully in offline mode upon network failure when keychain manager contains keys',
479+
() async {
480+
KeyChainManager mockKeyChainManager = MockKeyChainManager();
481+
RSAKeypair pkamKeyPair = KeyChainManager.getInstance().generateKeyPair();
482+
RSAKeypair encryptionKeyPair =
483+
KeyChainManager.getInstance().generateKeyPair();
484+
String selfEncryptionKey = KeyChainManager.getInstance().generateAESKey();
485+
AtAuthService atAuthService =
486+
AtClientMobile.authService(atSign, atClientPreference);
487+
(atAuthService as AtAuthServiceImpl).keyChainManager =
488+
mockKeyChainManager;
489+
490+
AtsignKey atsignKey = AtsignKey(
491+
atSign: atSign,
492+
encryptionPublicKey: encryptionKeyPair.publicKey.toString());
493+
494+
// Mock object to return keys from keychain manager
495+
when(() => mockKeyChainManager.readAtsign(name: atSign))
496+
.thenAnswer((_) => Future.value(atsignKey));
497+
498+
AtAuthRequest atAuthRequest = AtAuthRequest(atSign);
499+
atAuthRequest.atAuthKeys = AtAuthKeys()
500+
..apkamPrivateKey = pkamKeyPair.privateKey.toString()
501+
..apkamPublicKey = pkamKeyPair.publicKey.toString()
502+
..defaultEncryptionPublicKey = encryptionKeyPair.publicKey.toString()
503+
..defaultEncryptionPrivateKey = encryptionKeyPair.privateKey.toString()
504+
..defaultSelfEncryptionKey = selfEncryptionKey
505+
..enrollmentId = '123';
506+
507+
AtAuthResponse atAuthResponse =
508+
await atAuthService.authenticate(atAuthRequest);
509+
510+
expect(atAuthResponse.isSuccessful, true);
511+
expect(atAuthResponse.atSign, atSign);
512+
expect(atAuthResponse.atAuthKeys?.apkamPublicKey,
513+
pkamKeyPair.publicKey.toString());
514+
expect(atAuthResponse.atAuthKeys?.apkamPrivateKey,
515+
pkamKeyPair.privateKey.toString());
516+
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPrivateKey,
517+
encryptionKeyPair.privateKey.toString());
518+
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPublicKey,
519+
encryptionKeyPair.publicKey.toString());
520+
expect(atAuthResponse.atAuthKeys?.defaultSelfEncryptionKey,
521+
selfEncryptionKey);
522+
});
523+
524+
test(
525+
'A test to verify atClient initialization fails when network is offline and keychain manager does not have keys',
526+
() async {
527+
KeyChainManager mockKeyChainManager = MockKeyChainManager();
528+
RSAKeypair pkamKeyPair = KeyChainManager.getInstance().generateKeyPair();
529+
RSAKeypair encryptionKeyPair =
530+
KeyChainManager.getInstance().generateKeyPair();
531+
AtAuthService atAuthService =
532+
AtClientMobile.authService(atSign, atClientPreference);
533+
(atAuthService as AtAuthServiceImpl).keyChainManager =
534+
mockKeyChainManager;
535+
536+
AtsignKey atsignKey = AtsignKey(atSign: atSign, encryptionPublicKey: '');
537+
538+
// Mock object to return keys from keychain manager
539+
when(() => mockKeyChainManager.readAtsign(name: atSign))
540+
.thenAnswer((_) => Future.value(atsignKey));
541+
542+
AtAuthRequest atAuthRequest = AtAuthRequest(atSign);
543+
atAuthRequest.atAuthKeys = AtAuthKeys()
544+
..apkamPrivateKey = pkamKeyPair.privateKey.toString()
545+
..apkamPublicKey = pkamKeyPair.publicKey.toString()
546+
..defaultEncryptionPublicKey = encryptionKeyPair.publicKey.toString()
547+
..defaultEncryptionPrivateKey = encryptionKeyPair.privateKey.toString()
548+
..defaultSelfEncryptionKey =
549+
KeyChainManager.getInstance().generateAESKey();
550+
551+
AtAuthResponse atAuthResponse =
552+
await atAuthService.authenticate(atAuthRequest);
553+
554+
expect(atAuthResponse.isSuccessful, false);
555+
});
556+
557+
test(
558+
'A test to verify authentication is successful when pkamAuthentication returns true',
559+
() async {
560+
KeyChainManager mockKeyChainManager = MockKeyChainManager();
561+
AtLookUp mockAtLookup = MockAtLookUp();
562+
563+
RSAKeypair pkamKeyPair = KeyChainManager.getInstance().generateKeyPair();
564+
RSAKeypair encryptionKeyPair =
565+
KeyChainManager.getInstance().generateKeyPair();
566+
String selfEncryptionKey = KeyChainManager.getInstance().generateAESKey();
567+
AtAuthService atAuthService = AtClientMobile.authService(
568+
atSign, atClientPreference,
569+
atLookUp: mockAtLookup);
570+
(atAuthService as AtAuthServiceImpl).keyChainManager =
571+
mockKeyChainManager;
572+
573+
AtsignKey atsignKey = AtsignKey(
574+
atSign: atSign,
575+
encryptionPublicKey: encryptionKeyPair.publicKey.toString());
576+
577+
// Mock object to return keys from keychain manager
578+
when(() => mockKeyChainManager.readAtsign(name: atSign))
579+
.thenAnswer((_) => Future.value(atsignKey));
580+
581+
when(() => mockAtLookup.pkamAuthenticate(enrollmentId: '123'))
582+
.thenAnswer((_) => Future.value(true));
583+
584+
AtAuthRequest atAuthRequest = AtAuthRequest(atSign);
585+
atAuthRequest.atAuthKeys = AtAuthKeys()
586+
..apkamPrivateKey = pkamKeyPair.privateKey.toString()
587+
..apkamPublicKey = pkamKeyPair.publicKey.toString()
588+
..defaultEncryptionPublicKey = encryptionKeyPair.publicKey.toString()
589+
..defaultEncryptionPrivateKey = encryptionKeyPair.privateKey.toString()
590+
..defaultSelfEncryptionKey = selfEncryptionKey
591+
..enrollmentId = '123';
592+
593+
AtAuthResponse atAuthResponse =
594+
await atAuthService.authenticate(atAuthRequest);
595+
596+
expect(atAuthResponse.isSuccessful, true);
597+
expect(atAuthResponse.atSign, atSign);
598+
expect(atAuthResponse.atAuthKeys?.enrollmentId, '123');
599+
expect(atAuthResponse.atAuthKeys?.apkamPublicKey,
600+
pkamKeyPair.publicKey.toString());
601+
expect(atAuthResponse.atAuthKeys?.apkamPrivateKey,
602+
pkamKeyPair.privateKey.toString());
603+
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPrivateKey,
604+
encryptionKeyPair.privateKey.toString());
605+
expect(atAuthResponse.atAuthKeys?.defaultEncryptionPublicKey,
606+
encryptionKeyPair.publicKey.toString());
607+
expect(atAuthResponse.atAuthKeys?.defaultSelfEncryptionKey,
608+
selfEncryptionKey);
609+
});
610+
});
467611
}
468612

469613
void tearDownMethod(MockEnrollmentBiometricStorageFile mockBiometricStorageFile,

0 commit comments

Comments
 (0)