diff --git a/packages/at_auth/lib/at_auth.dart b/packages/at_auth/lib/at_auth.dart index bf1d37b5..1b2a55db 100644 --- a/packages/at_auth/lib/at_auth.dart +++ b/packages/at_auth/lib/at_auth.dart @@ -13,3 +13,5 @@ export 'src/enroll/at_enrollment_base.dart'; export 'src/enroll/at_enrollment_impl.dart'; export 'src/enroll/at_enrollment_response.dart'; export 'src/enroll/at_enrollment_request.dart'; +export 'src/enroll/at_initial_enrollment_request.dart'; +export 'src/enroll/at_new_enrollment_request.dart'; diff --git a/packages/at_auth/lib/src/at_auth_impl.dart b/packages/at_auth/lib/src/at_auth_impl.dart index 0e2818f4..8c8dbf25 100644 --- a/packages/at_auth/lib/src/at_auth_impl.dart +++ b/packages/at_auth/lib/src/at_auth_impl.dart @@ -6,6 +6,7 @@ import 'package:at_auth/src/at_auth_base.dart'; import 'package:at_auth/src/auth/cram_authenticator.dart'; import 'package:at_auth/src/auth/pkam_authenticator.dart'; import 'package:at_auth/src/enroll/at_enrollment_base.dart'; +import 'package:at_auth/src/enroll/at_initial_enrollment_request.dart'; import 'package:at_auth/src/keys/at_auth_keys.dart'; import 'package:at_auth/src/onboard/at_onboarding_request.dart'; import 'package:at_auth/src/onboard/at_onboarding_response.dart'; @@ -228,13 +229,16 @@ class AtAuthImpl implements AtAuth { encryptionAlgorithm: symmetricEncryptionAlgo, iv: AtChopsUtil.generateIVLegacy()) .result; - var enrollRequestBuilder = AtEnrollmentRequest.request() + _logger.finer('apkamPublicKey: ${atAuthKeys.apkamPublicKey}'); + var enrollRequestBuilder = AtInitialEnrollmentRequestBuilder() ..setAppName(atOnboardingRequest.appName) ..setDeviceName(atOnboardingRequest.deviceName) ..setEncryptedDefaultEncryptionPrivateKey( encryptedDefaultEncryptionPrivateKey) ..setEncryptedDefaultSelfEncryptionKey(encryptedDefaultSelfEncryptionKey) - ..setApkamPublicKey(atAuthKeys.apkamPublicKey); + ..setApkamPublicKey(atAuthKeys.apkamPublicKey) + ..setAtAuthKeys(atAuthKeys) + ..setEnrollOperationEnum(EnrollOperationEnum.request); atEnrollmentBase ??= AtEnrollmentImpl(atOnboardingRequest.atSign); AtEnrollmentResponse enrollmentResponse; try { diff --git a/packages/at_auth/lib/src/enroll/at_enrollment_impl.dart b/packages/at_auth/lib/src/enroll/at_enrollment_impl.dart index 088b1277..2b67d935 100644 --- a/packages/at_auth/lib/src/enroll/at_enrollment_impl.dart +++ b/packages/at_auth/lib/src/enroll/at_enrollment_impl.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:at_auth/at_auth.dart'; +import 'package:at_auth/src/enroll/at_enrollment_notification_request.dart'; import 'package:at_chops/at_chops.dart'; import 'package:at_commons/at_builders.dart'; import 'package:at_commons/at_commons.dart'; @@ -22,38 +23,55 @@ class AtEnrollmentImpl implements AtEnrollmentBase { @override Future submitEnrollment( AtEnrollmentRequest atEnrollmentRequest, AtLookUp atLookUp) async { + switch (atEnrollmentRequest.runtimeType) { + case AtInitialEnrollmentRequest: + return await _initialClientEnrollment( + atEnrollmentRequest as AtInitialEnrollmentRequest, atLookUp); + case AtNewEnrollmentRequest: + return await _newClientEnrollment( + atEnrollmentRequest as AtNewEnrollmentRequest, atLookUp); + default: + throw AtEnrollmentException( + 'Invalid AtEnrollmentRequest type: ${atEnrollmentRequest.runtimeType}'); + } + } + + Future _initialClientEnrollment( + AtInitialEnrollmentRequest atInitialEnrollmentRequest, + AtLookUp atLookUp) async { + _logger.finer('inside initialClientEnrollment'); + final atAuthKeys = atInitialEnrollmentRequest.atAuthKeys; + var enrollVerbBuilder = createEnrollVerbBuilder(atInitialEnrollmentRequest); + var enrollResult = await _executeEnrollCommand(enrollVerbBuilder, atLookUp); + _logger.finer('enrollResult: $enrollResult'); + var enrollJson = jsonDecode(enrollResult); + var enrollmentIdFromServer = enrollJson[AtConstants.enrollmentId]; + var enrollStatus = getEnrollStatusFromString(enrollJson['status']); + atAuthKeys!.enrollmentId = enrollmentIdFromServer; + return AtEnrollmentResponse(enrollmentIdFromServer, enrollStatus) + ..atAuthKeys = atAuthKeys; + } + + Future _newClientEnrollment( + AtNewEnrollmentRequest atNewEnrollmentRequest, AtLookUp atLookUp) async { _logger.info('Generating APKAM encryption keypair and APKAM symmetric key'); AtPkamKeyPair atPkamKeyPair = AtChopsUtil.generateAtPkamKeyPair(); SymmetricKey apkamSymmetricKey = AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); - - return await enrollInternal( - atEnrollmentRequest, atLookUp, atPkamKeyPair, apkamSymmetricKey); - } - - @visibleForTesting - Future enrollInternal( - AtEnrollmentRequest atEnrollmentRequest, - AtLookUp atLookUp, - AtPkamKeyPair atPkamKeyPair, - SymmetricKey apkamSymmetricKey) async { + // default flow String defaultEncryptionPublicKey = await _getDefaultEncryptionPublicKey(atLookUp); // Encrypting the Encryption Public key with APKAM Symmetric key. String encryptedApkamSymmetricKey = RSAPublicKey.fromString(defaultEncryptionPublicKey) .encrypt(apkamSymmetricKey.key); - - EnrollResponse enrollmentResponse = await _sendEnrollmentRequest( - atLookUp, - atEnrollmentRequest.appName, - atEnrollmentRequest.deviceName, - atEnrollmentRequest.otp, - atEnrollmentRequest.namespaces, - atPkamKeyPair.atPublicKey.publicKey, - encryptedApkamSymmetricKey, - ); - + var enrollVerbBuilder = createEnrollVerbBuilder(atNewEnrollmentRequest, + atPkamKeyPair: atPkamKeyPair, + encryptedApkamSymmetricKey: encryptedApkamSymmetricKey); + var enrollResult = await _executeEnrollCommand(enrollVerbBuilder, atLookUp); + var enrollJson = jsonDecode(enrollResult); + var enrollmentIdFromServer = enrollJson[AtConstants.enrollmentId]; + var enrollStatus = getEnrollStatusFromString(enrollJson['status']); AtChopsKeys atChopsKeys = AtChopsKeys.create( AtEncryptionKeyPair.create(defaultEncryptionPublicKey, ''), AtPkamKeyPair.create(atPkamKeyPair.atPublicKey.publicKey, @@ -66,13 +84,10 @@ class AtEnrollmentImpl implements AtEnrollmentBase { ..apkamPublicKey = atPkamKeyPair.atPublicKey.publicKey ..defaultEncryptionPublicKey = defaultEncryptionPublicKey ..apkamSymmetricKey = apkamSymmetricKey.key - ..enrollmentId = enrollmentResponse.enrollmentId; + ..enrollmentId = enrollmentIdFromServer; - // The EnrollmentSubmissionResponse has atChopsKeys which contains the APKAM - // keys and APKAM Symmetric key which will be persisted by the client requesting the - // enrollment. - AtEnrollmentResponse atEnrollmentResponse = AtEnrollmentResponse( - enrollmentResponse.enrollmentId, enrollmentResponse.enrollStatus); + AtEnrollmentResponse atEnrollmentResponse = + AtEnrollmentResponse(enrollmentIdFromServer, enrollStatus); atEnrollmentResponse.atAuthKeys = atAuthKeys; return atEnrollmentResponse; } @@ -90,43 +105,15 @@ class AtEnrollmentImpl implements AtEnrollmentBase { return defaultEncryptionPublicKey; } - Future _sendEnrollmentRequest( - AtLookUp atLookUp, - String? appName, - String? deviceName, - String? otp, - Map? namespaces, - String apkamPublicKey, - String encryptedApkamSymmetricKey, - ) async { - var enrollVerbBuilder = EnrollVerbBuilder() - ..appName = appName - ..deviceName = deviceName - ..namespaces = namespaces - ..otp = otp - ..apkamPublicKey = apkamPublicKey - ..encryptedAPKAMSymmetricKey = encryptedApkamSymmetricKey; - var enrollResult = - await atLookUp.executeCommand(enrollVerbBuilder.buildCommand()); - if (enrollResult == null || - enrollResult.isEmpty || - enrollResult.startsWith('error:')) { - throw AtEnrollmentException( - 'Enrollment response from server: $enrollResult'); - } - enrollResult = enrollResult.replaceFirst('data:', ''); - var enrollJson = jsonDecode(enrollResult); - var enrollmentIdFromServer = enrollJson[AtConstants.enrollmentId]; - - return EnrollResponse(enrollmentIdFromServer, - getEnrollStatusFromString(enrollJson['status'])); - } - @override Future manageEnrollmentApproval( AtEnrollmentRequest atEnrollmentRequest, AtLookUp atLookUp) { switch (atEnrollmentRequest.enrollOperationEnum) { case EnrollOperationEnum.approve: + if (atEnrollmentRequest is! AtEnrollmentNotificationRequest) { + throw AtEnrollmentException( + 'Invalid atEnrollmentRequest type: $atEnrollmentRequest. Please pass AtEnrollmentNotificationRequest'); + } return _handleApproveOperation(atEnrollmentRequest, atLookUp); case EnrollOperationEnum.deny: return _handleDenyOperation(atEnrollmentRequest, atLookUp); @@ -136,16 +123,17 @@ class AtEnrollmentImpl implements AtEnrollmentBase { } Future _handleApproveOperation( - AtEnrollmentRequest atEnrollmentRequest, AtLookUp atLookUp) async { + AtEnrollmentNotificationRequest atEnrollmentNotificationRequest, + AtLookUp atLookUp) async { // Decrypt the encrypted APKAM Symmetric key var defaultEncryptionPrivateKey = RSAPrivateKey.fromString(atLookUp .atChops!.atChopsKeys.atEncryptionKeyPair!.atPrivateKey.privateKey); var apkamSymmetricKey = defaultEncryptionPrivateKey - .decrypt(atEnrollmentRequest.encryptedAPKAMSymmetricKey!); + .decrypt(atEnrollmentNotificationRequest.encryptedApkamSymmetricKey); atLookUp.atChops?.atChopsKeys.apkamSymmetricKey = AESKey(apkamSymmetricKey); String command = 'enroll:approve:${jsonEncode({ - 'enrollmentId': atEnrollmentRequest.enrollmentId, + 'enrollmentId': atEnrollmentNotificationRequest.enrollmentId, 'encryptedDefaultEncryptedPrivateKey': atLookUp.atChops ?.encryptString( atLookUp.atChops!.atChopsKeys.atEncryptionKeyPair! @@ -203,4 +191,46 @@ class AtEnrollmentImpl implements AtEnrollmentBase { enrollmentJsonMap['enrollmentId'], enrollmentJsonMap['status']); return enrollmentResponse; } + + @visibleForTesting + /// Creates a verb builder instance based on the [request] type + EnrollVerbBuilder createEnrollVerbBuilder( + AtEnrollmentRequest request, { + AtPkamKeyPair? atPkamKeyPair, + String? encryptedApkamSymmetricKey, + }) { + var enrollVerbBuilder = EnrollVerbBuilder() + ..appName = request.appName + ..deviceName = request.deviceName + ..namespaces = request.namespaces; + + if (request is AtInitialEnrollmentRequest) { + enrollVerbBuilder + ..encryptedDefaultEncryptionPrivateKey = + request.encryptedDefaultEncryptionPrivateKey + ..encryptedDefaultSelfEncryptionKey = + request.encryptedDefaultSelfEncryptionKey + ..apkamPublicKey = request.apkamPublicKey; + } else if (request is AtNewEnrollmentRequest) { + enrollVerbBuilder + ..otp = request.otp + ..apkamPublicKey = atPkamKeyPair!.atPublicKey.publicKey + ..encryptedAPKAMSymmetricKey = encryptedApkamSymmetricKey; + } + + return enrollVerbBuilder; + } + + Future _executeEnrollCommand( + EnrollVerbBuilder enrollVerbBuilder, AtLookUp atLookUp) async { + var enrollResult = + await atLookUp.executeCommand(enrollVerbBuilder.buildCommand()); + if (enrollResult == null || + enrollResult.isEmpty || + enrollResult.startsWith('error:')) { + throw AtEnrollmentException( + 'Enrollment response from server: $enrollResult'); + } + return enrollResult.replaceFirst('data:', ''); + } } diff --git a/packages/at_auth/lib/src/enroll/at_enrollment_notification_request.dart b/packages/at_auth/lib/src/enroll/at_enrollment_notification_request.dart new file mode 100644 index 00000000..00ac8fa5 --- /dev/null +++ b/packages/at_auth/lib/src/enroll/at_enrollment_notification_request.dart @@ -0,0 +1,33 @@ +import 'package:at_auth/src/enroll/at_enrollment_request.dart'; + +/// In APKAM approval flow, use this class from a privileged client to set attributes required for enrollment approval. +/// Once a notification is received on the privileged client which can approve enrollment notifications from new devices, +/// use [AtEnrollmentNotificationRequestBuilder] to create [AtEnrollmentNotificationRequest] +class AtEnrollmentNotificationRequest extends AtEnrollmentRequest { + String _encryptedApkamSymmetricKey; + + String get encryptedApkamSymmetricKey => _encryptedApkamSymmetricKey; + + AtEnrollmentNotificationRequest.builder( + AtEnrollmentNotificationRequestBuilder + atEnrollmentNotificationRequestBuilder) + : _encryptedApkamSymmetricKey = + atEnrollmentNotificationRequestBuilder._encryptedApkamSymmetricKey, + super.builder(atEnrollmentNotificationRequestBuilder); +} + +class AtEnrollmentNotificationRequestBuilder + extends AtEnrollmentRequestBuilder { + late String _encryptedApkamSymmetricKey; + + AtEnrollmentNotificationRequestBuilder setEncryptedApkamSymmetricKey( + String encryptedApkamSymmetricKey) { + _encryptedApkamSymmetricKey = encryptedApkamSymmetricKey; + return this; + } + + /// Builds and returns an instance of [AtEnrollmentNotificationRequest]. + AtEnrollmentNotificationRequest build() { + return AtEnrollmentNotificationRequest.builder(this); + } +} diff --git a/packages/at_auth/lib/src/enroll/at_enrollment_request.dart b/packages/at_auth/lib/src/enroll/at_enrollment_request.dart index 655f322a..2424858e 100644 --- a/packages/at_auth/lib/src/enroll/at_enrollment_request.dart +++ b/packages/at_auth/lib/src/enroll/at_enrollment_request.dart @@ -1,57 +1,44 @@ +import 'package:at_auth/at_auth.dart'; import 'package:at_commons/at_commons.dart'; -/// Represents an enrollment request for APKAM. +/// Base class containing common attributes for enrollment requests either from first onboarding client with enrollment enabled +/// or a new client requesting enrollment. class AtEnrollmentRequest { final String? _appName; final String? _deviceName; - final String? _otp; final Map? _namespaces; - final EnrollOperationEnum _enrollOperationEnum; + EnrollOperationEnum _enrollOperationEnum = EnrollOperationEnum.request; final String? _enrollmentId; + final String? _apkamPublicKey; - final String? _encryptedAPKAMSymmetricKey; - final String? _encryptedDefaultEncryptionPrivateKey; - final String? _encryptedDefaultSelfEncryptionKey; + String? get apkamPublicKey => _apkamPublicKey; + String? get appName => _appName; String? get deviceName => _deviceName; - String? get otp => _otp; - Map? get namespaces => _namespaces; EnrollOperationEnum get enrollOperationEnum => _enrollOperationEnum; String? get enrollmentId => _enrollmentId; - String? get encryptedAPKAMSymmetricKey => _encryptedAPKAMSymmetricKey; - - String? get encryptedDefaultEncryptionPrivateKey => - _encryptedDefaultEncryptionPrivateKey; + final AtAuthKeys? _atAuthKeys; - String? get encryptedDefaultSelfEncryptionKey => - _encryptedDefaultSelfEncryptionKey; + AtAuthKeys? get atAuthKeys => _atAuthKeys; - String? get apkamPublicKey => _apkamPublicKey; - - AtEnrollmentRequest._builder( + AtEnrollmentRequest.builder( AtEnrollmentRequestBuilder atEnrollmentRequestBuilder) : _appName = atEnrollmentRequestBuilder._appName, _deviceName = atEnrollmentRequestBuilder._deviceName, _namespaces = atEnrollmentRequestBuilder._namespaces, - _otp = atEnrollmentRequestBuilder._otp, _enrollOperationEnum = atEnrollmentRequestBuilder._enrollmentOperationEnum, _enrollmentId = atEnrollmentRequestBuilder._enrollmentId, - _encryptedAPKAMSymmetricKey = - atEnrollmentRequestBuilder._encryptedAPKAMSymmetricKey, - _encryptedDefaultEncryptionPrivateKey = - atEnrollmentRequestBuilder._encryptedDefaultEncryptionPrivateKey, - _encryptedDefaultSelfEncryptionKey = - atEnrollmentRequestBuilder._encryptedDefaultSelfEncryptionKey, - _apkamPublicKey = atEnrollmentRequestBuilder._apkamPublicKey; + _apkamPublicKey = atEnrollmentRequestBuilder._apkamPublicKey, + _atAuthKeys = atEnrollmentRequestBuilder._atAuthKeys; /// Creates an [AtEnrollmentRequestBuilder] for building enrollment requests. /// @@ -111,13 +98,10 @@ class AtEnrollmentRequestBuilder { String? _appName; String? _deviceName; Map _namespaces = {}; - String? _otp; - late EnrollOperationEnum _enrollmentOperationEnum; + EnrollOperationEnum _enrollmentOperationEnum = EnrollOperationEnum.request; String? _enrollmentId; - String? _encryptedAPKAMSymmetricKey; - String? _encryptedDefaultEncryptionPrivateKey; - String? _encryptedDefaultSelfEncryptionKey; String? _apkamPublicKey; + AtAuthKeys? _atAuthKeys; AtEnrollmentRequestBuilder setAppName(String? appName) { _appName = appName; @@ -134,42 +118,29 @@ class AtEnrollmentRequestBuilder { return this; } - AtEnrollmentRequestBuilder setOtp(String? otp) { - _otp = otp; - return this; - } - AtEnrollmentRequestBuilder setEnrollmentId(String? enrollmentId) { _enrollmentId = enrollmentId; return this; } - AtEnrollmentRequestBuilder setEncryptedAPKAMSymmetricKey( - String? encryptedAPKAMSymmetricKey) { - _encryptedAPKAMSymmetricKey = encryptedAPKAMSymmetricKey; - return this; - } - - AtEnrollmentRequestBuilder setEncryptedDefaultEncryptionPrivateKey( - String? encryptedDefaultEncryptionPrivateKey) { - _encryptedDefaultEncryptionPrivateKey = - encryptedDefaultEncryptionPrivateKey; + AtEnrollmentRequestBuilder setApkamPublicKey(String? apkamPublicKey) { + _apkamPublicKey = apkamPublicKey; return this; } - AtEnrollmentRequestBuilder setEncryptedDefaultSelfEncryptionKey( - String? encryptedDefaultSelfEncryptionKey) { - _encryptedDefaultSelfEncryptionKey = encryptedDefaultSelfEncryptionKey; + AtEnrollmentRequestBuilder setEnrollOperationEnum( + EnrollOperationEnum enrollOperationEnum) { + _enrollmentOperationEnum = enrollOperationEnum; return this; } - AtEnrollmentRequestBuilder setApkamPublicKey(String? apkamPublicKey) { - _apkamPublicKey = apkamPublicKey; + AtEnrollmentRequestBuilder setAtAuthKeys(AtAuthKeys? atAuthKeys) { + _atAuthKeys = atAuthKeys; return this; } /// Builds and returns an instance of [AtEnrollmentRequest]. AtEnrollmentRequest build() { - return AtEnrollmentRequest._builder(this); + return AtEnrollmentRequest.builder(this); } } diff --git a/packages/at_auth/lib/src/enroll/at_initial_enrollment_request.dart b/packages/at_auth/lib/src/enroll/at_initial_enrollment_request.dart new file mode 100644 index 00000000..49584e5e --- /dev/null +++ b/packages/at_auth/lib/src/enroll/at_initial_enrollment_request.dart @@ -0,0 +1,46 @@ +import 'package:at_auth/src/enroll/at_enrollment_request.dart'; + +/// Class for attributes required specifically for enrollment from the first onboarding client that +/// has enableEnrollment flag set to true from client side in preferences. +/// Default encryption private key and default self encryption keys are encrypted using APKAM symmetric key generated for the onboarding client. +class AtInitialEnrollmentRequest extends AtEnrollmentRequest { + final String _encryptedDefaultEncryptionPrivateKey; + final String _encryptedDefaultSelfEncryptionKey; + + AtInitialEnrollmentRequest.builder( + AtInitialEnrollmentRequestBuilder atInitialEnrollmentRequestBuilder) + : _encryptedDefaultEncryptionPrivateKey = + atInitialEnrollmentRequestBuilder + ._encryptedDefaultEncryptionPrivateKey, + _encryptedDefaultSelfEncryptionKey = atInitialEnrollmentRequestBuilder + ._encryptedDefaultSelfEncryptionKey, + super.builder(atInitialEnrollmentRequestBuilder); + + String get encryptedDefaultEncryptionPrivateKey => + _encryptedDefaultEncryptionPrivateKey; + + String get encryptedDefaultSelfEncryptionKey => + _encryptedDefaultSelfEncryptionKey; +} + +class AtInitialEnrollmentRequestBuilder extends AtEnrollmentRequestBuilder { + late String _encryptedDefaultEncryptionPrivateKey; + late String _encryptedDefaultSelfEncryptionKey; + AtEnrollmentRequestBuilder setEncryptedDefaultEncryptionPrivateKey( + String encryptedDefaultEncryptionPrivateKey) { + _encryptedDefaultEncryptionPrivateKey = + encryptedDefaultEncryptionPrivateKey; + return this; + } + + AtEnrollmentRequestBuilder setEncryptedDefaultSelfEncryptionKey( + String encryptedDefaultSelfEncryptionKey) { + _encryptedDefaultSelfEncryptionKey = encryptedDefaultSelfEncryptionKey; + return this; + } + + /// Builds and returns an instance of [AtInitialEnrollmentRequest]. + AtInitialEnrollmentRequest build() { + return AtInitialEnrollmentRequest.builder(this); + } +} diff --git a/packages/at_auth/lib/src/enroll/at_new_enrollment_request.dart b/packages/at_auth/lib/src/enroll/at_new_enrollment_request.dart new file mode 100644 index 00000000..7051b274 --- /dev/null +++ b/packages/at_auth/lib/src/enroll/at_new_enrollment_request.dart @@ -0,0 +1,27 @@ +import 'package:at_auth/src/enroll/at_enrollment_request.dart'; + +/// Class for attributes required specifically for new enrollment requests from client. +class AtNewEnrollmentRequest extends AtEnrollmentRequest { + String _otp; + + AtNewEnrollmentRequest.builder( + AtNewEnrollmentRequestBuilder atNewEnrollmentRequestBuilder) + : _otp = atNewEnrollmentRequestBuilder._otp, + super.builder(atNewEnrollmentRequestBuilder); + + String get otp => _otp; +} + +class AtNewEnrollmentRequestBuilder extends AtEnrollmentRequestBuilder { + late String _otp; + + AtNewEnrollmentRequestBuilder setOtp(String otp) { + _otp = otp; + return this; + } + + /// Builds and returns an instance of [AtNewEnrollmentRequest]. + AtNewEnrollmentRequest build() { + return AtNewEnrollmentRequest.builder(this); + } +} diff --git a/packages/at_auth/test/enrollment_test.dart b/packages/at_auth/test/enrollment_test.dart index fed46edb..556ee53c 100644 --- a/packages/at_auth/test/enrollment_test.dart +++ b/packages/at_auth/test/enrollment_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:at_auth/at_auth.dart'; +import 'package:at_auth/src/enroll/at_enrollment_notification_request.dart'; import 'package:at_chops/at_chops.dart'; import 'package:at_commons/at_builders.dart'; import 'package:at_commons/at_commons.dart'; @@ -80,52 +81,134 @@ void main() { when(() => (mockAtLookUp as AtLookupImpl).close()) .thenAnswer((_) async => ()); - AtEnrollmentRequest atEnrollmentRequest = (AtEnrollmentRequest.request() + AtNewEnrollmentRequestBuilder atNewEnrollmentRequestBuilder = + AtNewEnrollmentRequestBuilder() ..setAppName('wavi') ..setDeviceName('pixel') - ..setOtp('12345') - ..setNamespaces({'wavi': 'rw'})) - .build(); - - AtPkamKeyPair atPkamKeyPair = - AtPkamKeyPair.create(apkamPublicKey, apkamPrivateKey); - SymmetricKey symmetricKey = AESKey(apkamSymmetricKey); - - AtEnrollmentResponse enrollmentSubmissionResponse = - await atEnrollmentServiceImpl.enrollInternal( - atEnrollmentRequest, mockAtLookUp, atPkamKeyPair, symmetricKey); - expect(enrollmentSubmissionResponse.enrollmentId, '123'); - expect(enrollmentSubmissionResponse.enrollStatus, EnrollStatus.pending); + ..setNamespaces({'wavi': 'rw'}) + ..setOtp('A123FE') + ..setApkamPublicKey('testApkamPublicKey'); + AtNewEnrollmentRequest atNewEnrollmentRequest = + atNewEnrollmentRequestBuilder.build(); + + AtEnrollmentResponse atEnrollmentResponse = await atEnrollmentServiceImpl + .submitEnrollment(atNewEnrollmentRequest, mockAtLookUp); + expect(atEnrollmentResponse.enrollmentId, '123'); + expect(atEnrollmentResponse.enrollStatus, EnrollStatus.pending); }); group('A group of test related to AtEnrollmentBuilder', () { - test('A test to verify generation of enrollment request', () { - AtEnrollmentRequestBuilder atEnrollmentRequestBuilder = - AtEnrollmentRequest.request() + test( + 'A test to verify generation of initial onboarding enrollment request - default operation request', + () { + AtInitialEnrollmentRequestBuilder atInitialEnrollmentRequestBuilder = + AtInitialEnrollmentRequestBuilder() ..setAppName('wavi') ..setDeviceName('pixel') - ..setOtp('ABC123') - ..setNamespaces({'wavi': 'rw'}); - AtEnrollmentRequest atEnrollmentRequest = - atEnrollmentRequestBuilder.build(); + ..setNamespaces({'wavi': 'rw'}) + ..setEncryptedDefaultEncryptionPrivateKey('testPrivateKey') + ..setEncryptedDefaultSelfEncryptionKey('testSelfKey') + ..setApkamPublicKey('testApkamPublicKey'); + AtInitialEnrollmentRequest atInitialEnrollmentRequest = + atInitialEnrollmentRequestBuilder.build(); + + expect(atInitialEnrollmentRequest.appName, 'wavi'); + expect(atInitialEnrollmentRequest.deviceName, 'pixel'); + expect(atInitialEnrollmentRequest.namespaces, {'wavi': 'rw'}); + expect(atInitialEnrollmentRequest.encryptedDefaultEncryptionPrivateKey, + 'testPrivateKey'); + expect(atInitialEnrollmentRequest.encryptedDefaultSelfEncryptionKey, + 'testSelfKey'); + expect(atInitialEnrollmentRequest.apkamPublicKey, 'testApkamPublicKey'); + expect(atInitialEnrollmentRequest.enrollOperationEnum, + EnrollOperationEnum.request); + }); + test( + 'A test to verify generation of initial onboarding enrollment request - set operation', + () { + AtInitialEnrollmentRequestBuilder atInitialEnrollmentRequestBuilder = + AtInitialEnrollmentRequestBuilder() + ..setAppName('wavi') + ..setDeviceName('pixel') + ..setNamespaces({'wavi': 'rw'}) + ..setEncryptedDefaultEncryptionPrivateKey('testPrivateKey') + ..setEncryptedDefaultSelfEncryptionKey('testSelfKey') + ..setApkamPublicKey('testApkamPublicKey') + ..setEnrollOperationEnum(EnrollOperationEnum.approve); + AtInitialEnrollmentRequest atInitialEnrollmentRequest = + atInitialEnrollmentRequestBuilder.build(); + + expect(atInitialEnrollmentRequest.appName, 'wavi'); + expect(atInitialEnrollmentRequest.deviceName, 'pixel'); + expect(atInitialEnrollmentRequest.namespaces, {'wavi': 'rw'}); + expect(atInitialEnrollmentRequest.encryptedDefaultEncryptionPrivateKey, + 'testPrivateKey'); + expect(atInitialEnrollmentRequest.encryptedDefaultSelfEncryptionKey, + 'testSelfKey'); + expect(atInitialEnrollmentRequest.apkamPublicKey, 'testApkamPublicKey'); + expect(atInitialEnrollmentRequest.enrollOperationEnum, + EnrollOperationEnum.approve); + }); + + test( + 'A test to verify generation of new enrollment request - default operation request', + () { + AtNewEnrollmentRequestBuilder atNewEnrollmentRequestBuilder = + AtNewEnrollmentRequestBuilder() + ..setAppName('wavi') + ..setDeviceName('pixel') + ..setNamespaces({'wavi': 'rw'}) + ..setOtp('A123FE') + ..setApkamPublicKey('testApkamPublicKey'); + AtNewEnrollmentRequest atNewEnrollmentRequest = + atNewEnrollmentRequestBuilder.build(); + + expect(atNewEnrollmentRequest.appName, 'wavi'); + expect(atNewEnrollmentRequest.deviceName, 'pixel'); + expect(atNewEnrollmentRequest.namespaces, {'wavi': 'rw'}); + expect(atNewEnrollmentRequest.apkamPublicKey, 'testApkamPublicKey'); + expect(atNewEnrollmentRequest.otp, 'A123FE'); + expect(atNewEnrollmentRequest.enrollOperationEnum, + EnrollOperationEnum.request); + }); - expect(atEnrollmentRequest.appName, 'wavi'); - expect(atEnrollmentRequest.deviceName, 'pixel'); - expect(atEnrollmentRequest.otp, 'ABC123'); - expect(atEnrollmentRequest.namespaces, {'wavi': 'rw'}); + test( + 'A test to verify generation of new enrollment request - set operation', + () { + AtNewEnrollmentRequestBuilder atNewEnrollmentRequestBuilder = + AtNewEnrollmentRequestBuilder() + ..setAppName('wavi') + ..setDeviceName('pixel') + ..setNamespaces({'wavi': 'rw'}) + ..setOtp('A123FE') + ..setApkamPublicKey('testApkamPublicKey') + ..setEnrollOperationEnum(EnrollOperationEnum.request); + AtNewEnrollmentRequest atNewEnrollmentRequest = + atNewEnrollmentRequestBuilder.build(); + + expect(atNewEnrollmentRequest.appName, 'wavi'); + expect(atNewEnrollmentRequest.deviceName, 'pixel'); + expect(atNewEnrollmentRequest.namespaces, {'wavi': 'rw'}); + expect(atNewEnrollmentRequest.apkamPublicKey, 'testApkamPublicKey'); + expect(atNewEnrollmentRequest.otp, 'A123FE'); + expect(atNewEnrollmentRequest.enrollOperationEnum, + EnrollOperationEnum.request); }); test('A test to verify generation of enrollment approval request', () { - AtEnrollmentRequestBuilder atEnrollmentRequestBuilder = - AtEnrollmentRequest.approve() + AtEnrollmentNotificationRequestBuilder atEnrollmentNotificationBuilder = + AtEnrollmentNotificationRequestBuilder() ..setEnrollmentId('ABC-123-ID') - ..setEncryptedAPKAMSymmetricKey('dummy-apkam-symmetric-key'); - AtEnrollmentRequest atEnrollmentRequest = - atEnrollmentRequestBuilder.build(); + ..setEncryptedApkamSymmetricKey('dummy-apkam-symmetric-key') + ..setEnrollOperationEnum(EnrollOperationEnum.approve); + AtEnrollmentNotificationRequest atEnrollmentNotificationRequest = + atEnrollmentNotificationBuilder.build(); - expect(atEnrollmentRequest.enrollmentId, 'ABC-123-ID'); - expect(atEnrollmentRequest.encryptedAPKAMSymmetricKey, + expect(atEnrollmentNotificationRequest.enrollmentId, 'ABC-123-ID'); + expect(atEnrollmentNotificationRequest.encryptedApkamSymmetricKey, 'dummy-apkam-symmetric-key'); + expect(atEnrollmentNotificationRequest.enrollOperationEnum, + EnrollOperationEnum.approve); }); test('A test to verify generation of enrollment deny request', () { @@ -137,6 +220,58 @@ void main() { expect(atEnrollmentRequest.enrollmentId, 'ABC-123-ID'); }); }); + group('Group of tests to check createEnrollVerbBuilder method', () { + test( + 'A test to verify createEnrollVerbBuilder for AtInitialEnrollmentRequest', + () { + var enrollmentImpl = AtEnrollmentImpl('@alice'); + AtInitialEnrollmentRequest request = (AtInitialEnrollmentRequestBuilder() + ..setAppName('TestApp') + ..setDeviceName('TestDevice') + ..setNamespaces({"wavi": "rw"}) + ..setEncryptedDefaultEncryptionPrivateKey('encryptedPrivateKey') + ..setEncryptedDefaultSelfEncryptionKey('encryptedSelfEncryptionKey') + ..setApkamPublicKey('apkamPublicKey')) + .build(); + + var result = enrollmentImpl.createEnrollVerbBuilder(request); + + expect(result.appName, equals('TestApp')); + expect(result.deviceName, equals('TestDevice')); + expect(result.namespaces, equals({'wavi': 'rw'})); + expect(result.encryptedDefaultEncryptionPrivateKey, + equals('encryptedPrivateKey')); + expect(result.encryptedDefaultSelfEncryptionKey, + equals('encryptedSelfEncryptionKey')); + expect(result.apkamPublicKey, equals('apkamPublicKey')); + expect(result.otp, isNull); + expect(result.encryptedAPKAMSymmetricKey, isNull); + }); + + test('A test for createEnrollVerbBuilder for AtNewEnrollmentRequest', () { + var request = (AtNewEnrollmentRequestBuilder() + ..setAppName('TestApp') + ..setDeviceName('TestDevice') + ..setNamespaces({"wavi": "rw", "contact": "r"}) + ..setOtp('A1CFG3') + ..setApkamPublicKey('apkamPublicKey')) + .build(); + + var enrollmentImpl = AtEnrollmentImpl('@alice'); + AtPkamKeyPair atPkamKeyPair = AtChopsUtil.generateAtPkamKeyPair(); + var result = enrollmentImpl.createEnrollVerbBuilder(request, + atPkamKeyPair: atPkamKeyPair); + + // Assert + expect(result.appName, equals('TestApp')); + expect(result.deviceName, equals('TestDevice')); + expect(result.namespaces, equals({"wavi": "rw", "contact": "r"})); + expect(result.otp, equals('A1CFG3')); + expect(result.apkamPublicKey, isNotNull); + expect(result.encryptedDefaultEncryptionPrivateKey, isNull); + expect(result.encryptedDefaultSelfEncryptionKey, isNull); + }); + }); } class LookUpVerbBuilderMatcher extends Matcher { diff --git a/packages/at_onboarding_cli/example/onboard.dart b/packages/at_onboarding_cli/example/onboard.dart index 6f58fff6..5235f298 100644 --- a/packages/at_onboarding_cli/example/onboard.dart +++ b/packages/at_onboarding_cli/example/onboard.dart @@ -31,5 +31,6 @@ ArgParser getArgParser() { ..addOption('atsign', abbr: 'a', help: 'the atsign you would like to auth with') ..addOption('cram', abbr: 'c', help: 'CRAM secret for the atsign') + ..addOption('atKeysPath', abbr: 'k', help: 'path to save keys file') ..addFlag('help', abbr: 'h', help: 'Usage instructions', negatable: false); } diff --git a/packages/at_onboarding_cli/example/util/atsign_preference.dart b/packages/at_onboarding_cli/example/util/atsign_preference.dart index 198cfd0e..9f925a23 100644 --- a/packages/at_onboarding_cli/example/util/atsign_preference.dart +++ b/packages/at_onboarding_cli/example/util/atsign_preference.dart @@ -1,8 +1,5 @@ -import 'dart:typed_data'; import 'package:at_client/src/preference/at_client_preference.dart'; import 'package:at_onboarding_cli/src/util/home_directory_util.dart'; -import 'package:at_utils/at_utils.dart'; -import 'dart:io'; class AtSignPreference { static AtClientPreference getAlicePreference( @@ -14,15 +11,6 @@ class AtSignPreference { HomeDirectoryUtil.getCommitLogPath(atSign, enrollmentId: enrollmentId); preference.isLocalStoreRequired = true; preference.rootDomain = 'vip.ve.atsign.zone'; - var hashFile = AtUtils.getShaForAtSign(atSign); - preference.keyStoreSecret = - _getKeyStoreSecret('${preference.hiveStoragePath}/$hashFile.hash'); return preference; } - - static List _getKeyStoreSecret(String filePath) { - var hiveSecretString = File(filePath).readAsStringSync(); - var secretAsUint8List = Uint8List.fromList(hiveSecretString.codeUnits); - return secretAsUint8List; - } } diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service.dart index 36df34ee..b10924ff 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service.dart @@ -21,7 +21,7 @@ abstract class AtOnboardingService { /// namespaces - key-value pair of namespace-access of the requesting client e.g {"wavi":"rw","contacts":"r"} /// pkamRetryIntervalMins - optional param which specifies interval in mins for pkam retry for this enrollment. /// The passed value will override the value in [AtOnboardingPreference] - Future enroll(String appName, String deviceName, String otp, + Future enroll(String appName, String deviceName, String otp, Map namespaces, {int? pkamRetryIntervalMins}); diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 22dc3d6a..5284dc66 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -10,7 +10,6 @@ import 'package:at_auth/at_auth.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; import 'package:at_server_status/at_server_status.dart'; import 'package:at_utils/at_utils.dart'; -import 'package:at_commons/at_builders.dart'; import 'package:at_onboarding_cli/src/factory/service_factories.dart'; import 'package:at_lookup/at_lookup.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; @@ -41,11 +40,13 @@ class AtOnboardingServiceImpl implements AtOnboardingService { /// a [DefaultAtServiceFactory] AtServiceFactory? atServiceFactory; + AtEnrollmentBase? _atEnrollmentBase; + AtOnboardingServiceImpl(atsign, this.atOnboardingPreference, {this.atServiceFactory, String? enrollmentId}) { // performs atSign format checks on the atSign _atSign = AtUtils.fixAtSign(atsign); - + _atEnrollmentBase ??= AtEnrollmentImpl(_atSign); // set default LocalStorage paths for this instance atOnboardingPreference.commitLogPath ??= HomeDirectoryUtil.getCommitLogPath(_atSign, enrollmentId: enrollmentId); @@ -137,8 +138,8 @@ class AtOnboardingServiceImpl implements AtOnboardingService { } @override - Future enroll(String appName, String deviceName, String otp, - Map namespaces, + Future enroll(String appName, String deviceName, + String otp, Map namespaces, {int? pkamRetryIntervalMins}) async { if (appName == null || deviceName == null) { throw AtEnrollmentException( @@ -146,50 +147,26 @@ class AtOnboardingServiceImpl implements AtOnboardingService { } pkamRetryIntervalMins ??= atOnboardingPreference.apkamAuthRetryDurationMins; final Duration retryInterval = Duration(minutes: pkamRetryIntervalMins); - logger.info('Generating apkam encryption keypair and apkam symmetric key'); - //1. Generate new apkam key pair and apkam symmetric key - var apkamKeyPair = generateRsaKeypair(); - var apkamSymmetricKey = generateAESKey(); AtLookupImpl atLookUpImpl = AtLookupImpl(_atSign, atOnboardingPreference.rootDomain, atOnboardingPreference.rootPort); - //2. Retrieve default encryption public key and encrypt apkam symmetric key - var defaultEncryptionPublicKey = - await _retrieveEncryptionPublicKey(atLookUpImpl); - var encryptedApkamSymmetricKey = EncryptionUtil.encryptKey( - apkamSymmetricKey, defaultEncryptionPublicKey); - - //3. Send enroll request to server - var enrollmentResponse = await _sendEnrollRequest( - appName, - deviceName, - otp, - namespaces, - apkamKeyPair.publicKey.toString(), - encryptedApkamSymmetricKey, - atLookUpImpl); + //2. Send enroll request to server + AtEnrollmentResponse enrollmentResponse = await _sendEnrollRequest( + appName, deviceName, otp, namespaces, atLookUpImpl); logger.finer('EnrollmentResponse from server: $enrollmentResponse'); - //4. Create at chops instance - var atChopsKeys = AtChopsKeys.create( - null, - AtPkamKeyPair.create(apkamKeyPair.publicKey.toString(), - apkamKeyPair.privateKey.toString())); - atLookUpImpl.atChops = AtChopsImpl(atChopsKeys); - // Pkam auth will be attempted asynchronously until enrollment is approved/denied _attemptPkamAuthAsync( - atLookUpImpl, - enrollmentResponse.enrollmentId, - retryInterval, - apkamSymmetricKey, - defaultEncryptionPublicKey, - apkamKeyPair); + atLookUpImpl, enrollmentResponse.enrollmentId, retryInterval); // Upon successful pkam auth, callback _listenToPkamSuccessStream will be invoked - _listenToPkamSuccessStream(atLookUpImpl, apkamSymmetricKey, - defaultEncryptionPublicKey, apkamKeyPair); + _listenToPkamSuccessStream( + atLookUpImpl, + enrollmentResponse.atAuthKeys!.apkamSymmetricKey!, + enrollmentResponse.atAuthKeys!.defaultEncryptionPublicKey!, + enrollmentResponse.atAuthKeys!.apkamPublicKey!, + enrollmentResponse.atAuthKeys!.apkamPrivateKey!); return enrollmentResponse; } @@ -198,7 +175,8 @@ class AtOnboardingServiceImpl implements AtOnboardingService { AtLookupImpl atLookUpImpl, String apkamSymmetricKey, String defaultEncryptionPublicKey, - RSAKeypair apkamKeyPair) { + String apkamPublicKey, + String apkamPrivateKey) { _onPkamSuccess.listen((enrollmentIdFromServer) async { logger.finer('_listenToPkamSuccessStream invoked'); var decryptedEncryptionPrivateKey = EncryptionUtil.decryptValue( @@ -215,8 +193,8 @@ class AtOnboardingServiceImpl implements AtOnboardingService { ..defaultEncryptionPublicKey = defaultEncryptionPublicKey ..apkamSymmetricKey = apkamSymmetricKey ..defaultSelfEncryptionKey = decryptedSelfEncryptionKey - ..apkamPublicKey = apkamKeyPair.publicKey.toString() - ..apkamPrivateKey = apkamKeyPair.privateKey.toString(); + ..apkamPublicKey = apkamPublicKey + ..apkamPrivateKey = apkamPrivateKey; logger.finer('Generating keys file for $enrollmentIdFromServer'); await _generateAtKeysFile(enrollmentIdFromServer, atAuthKeys); }); @@ -267,13 +245,8 @@ class AtOnboardingServiceImpl implements AtOnboardingService { return selfEncryptionKeyFromServer; } - Future _attemptPkamAuthAsync( - AtLookupImpl atLookUpImpl, - String enrollmentIdFromServer, - Duration retryInterval, - String apkamSymmetricKey, - String defaultEncryptionPublicKey, - RSAKeypair apkamKeyPair) async { + Future _attemptPkamAuthAsync(AtLookupImpl atLookUpImpl, + String enrollmentIdFromServer, Duration retryInterval) async { // Pkam auth will be retried until server approves/denies/expires the enrollment while (true) { logger.finer('Attempting pkam for $enrollmentIdFromServer'); @@ -311,35 +284,22 @@ class AtOnboardingServiceImpl implements AtOnboardingService { return false; } - Future _sendEnrollRequest( + Future _sendEnrollRequest( String appName, String deviceName, String otp, Map namespaces, - String apkamPublicKey, - String encryptedApkamSymmetricKey, AtLookupImpl atLookUpImpl) async { - var enrollVerbBuilder = EnrollVerbBuilder() - ..appName = appName - ..deviceName = deviceName - ..namespaces = namespaces - ..otp = otp - ..apkamPublicKey = apkamPublicKey - ..encryptedAPKAMSymmetricKey = encryptedApkamSymmetricKey; - var enrollResult = - await atLookUpImpl.executeCommand(enrollVerbBuilder.buildCommand()); - if (enrollResult == null || - enrollResult.isEmpty || - enrollResult.startsWith('error:')) { - throw AtEnrollmentException( - 'Enrollment response from server: $enrollResult'); - } - enrollResult = enrollResult.replaceFirst('data:', ''); - var enrollJson = jsonDecode(enrollResult); - var enrollmentIdFromServer = enrollJson[AtConstants.enrollmentId]; - logger.finer('enrollmentIdFromServer: $enrollmentIdFromServer'); - return EnrollResponse(enrollmentIdFromServer, - getEnrollStatusFromString(enrollJson['status'])); + var newClientEnrollmentRequest = (AtNewEnrollmentRequestBuilder() + ..setAppName(appName) + ..setDeviceName(deviceName) + ..setNamespaces(namespaces) + ..setOtp(otp) + ..setEnrollOperationEnum(EnrollOperationEnum.request)) + .build(); + logger.finer('calling at_enrollment_impl submit enrollment'); + return await _atEnrollmentBase! + .submitEnrollment(newClientEnrollmentRequest, atLookUpImpl); } ///write newly created encryption keypairs into atKeys file @@ -489,19 +449,6 @@ class AtOnboardingServiceImpl implements AtOnboardingService { return atAuthKeys; } - Future _retrieveEncryptionPublicKey(AtLookUp atLookupImpl) async { - var lookupVerbBuilder = LookupVerbBuilder() - ..atKey = 'publickey' - ..sharedBy = _atSign; - var lookupResult = await atLookupImpl.executeVerb(lookupVerbBuilder); - if (lookupResult == null || lookupResult.isEmpty) { - throw AtEnrollmentException( - 'Unable to lookup encryption public key. Server response is null/empty'); - } - var defaultEncryptionPublicKey = lookupResult.replaceFirst('data:', ''); - return defaultEncryptionPublicKey; - } - ///generates random RSA keypair RSAKeypair generateRsaKeypair() { return RSAKeypair.fromRandom(); diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index ef8c46ce..c7efb77b 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -28,6 +28,11 @@ dependencies: at_server_status: ^1.0.3 at_utils: ^3.0.15 +dependency_overrides: + at_auth: + path: ../at_auth + + dev_dependencies: lints: ^2.1.0 test: ^1.24.2 diff --git a/tests/at_onboarding_cli_functional_tests/pubspec.yaml b/tests/at_onboarding_cli_functional_tests/pubspec.yaml index 8c3f4246..67785dc4 100644 --- a/tests/at_onboarding_cli_functional_tests/pubspec.yaml +++ b/tests/at_onboarding_cli_functional_tests/pubspec.yaml @@ -16,6 +16,10 @@ dependencies: at_utils: ^3.0.15 at_lookup: ^3.0.40 +dependency_overrides: + at_auth: + path: ../../packages/at_auth + dev_dependencies: lints: ^1.0.0 test: ^1.17.2