From 4ffe975c68ab8751b94da0d1dc2c739cb94956f8 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Wed, 4 Sep 2024 15:00:00 +0530 Subject: [PATCH 01/11] fix: Introduce time duration for apkam keys to auto expire --- .../src/enroll/enroll_datastore_value.dart | 1 + .../src/enroll/enroll_datastore_value.g.dart | 6 ++- .../src/verb/handler/enroll_verb_handler.dart | 12 +++-- packages/at_secondary_server/pubspec.yaml | 8 +++ .../test/enroll_verb_test.dart | 53 +++++++++++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart index 2e964b2b5..224a6b7eb 100644 --- a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart +++ b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart @@ -16,6 +16,7 @@ class EnrollDataStoreValue { EnrollRequestType? requestType; EnrollApproval? approval; String? encryptedAPKAMSymmetricKey; + Duration? apkamKeysExpiryDuration; EnrollDataStoreValue( this.sessionId, this.appName, this.deviceName, this.apkamPublicKey); diff --git a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart index 80e348b57..9ff1d4e93 100644 --- a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart +++ b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart @@ -21,7 +21,10 @@ EnrollDataStoreValue _$EnrollDataStoreValueFromJson( ? null : EnrollApproval.fromJson(json['approval'] as Map) ..encryptedAPKAMSymmetricKey = - json['encryptedAPKAMSymmetricKey'] as String?; + json['encryptedAPKAMSymmetricKey'] as String? + ..apkamKeysExpiryDuration = (json['apkamExpiryInMillis'] == null) + ? null + : Duration(milliseconds: json['apkamExpiryInMillis']); Map _$EnrollDataStoreValueToJson( EnrollDataStoreValue instance) => @@ -34,6 +37,7 @@ Map _$EnrollDataStoreValueToJson( 'requestType': _$EnrollRequestTypeEnumMap[instance.requestType], 'approval': instance.approval, 'encryptedAPKAMSymmetricKey': instance.encryptedAPKAMSymmetricKey, + 'apkamExpiryInMillis': instance.apkamKeysExpiryDuration?.inMilliseconds, }; const _$EnrollRequestTypeEnumMap = { diff --git a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart index 6ce442a4b..f37b032c3 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart @@ -239,6 +239,12 @@ class EnrollVerbHandler extends AbstractVerbHandler { enrollParams.apkamPublicKey!); enrollmentValue.namespaces = enrollNamespaces; enrollmentValue.requestType = EnrollRequestType.newEnrollment; + + if (enrollParams.apkamKeysExpiryDuration != null) { + enrollmentValue.apkamKeysExpiryDuration = + enrollParams.apkamKeysExpiryDuration; + } + AtData enrollData; if (atConnection.metaData.authType != null && atConnection.metaData.authType == AuthType.cram) { @@ -599,9 +605,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { // Fetch the existing data AtMetaData? enrollMetaData = await keyStore.getMeta(enrollmentKey); // Update key with new data - // only update ttl, expiresAt in metadata to preserve all the other valid data fields - enrollMetaData?.ttl = 0; - enrollMetaData?.expiresAt = null; + // Update ttl value to support auto expiry of APKAM keys + enrollMetaData?.ttl = + enrollDataStoreValue.apkamKeysExpiryDuration?.inMilliseconds; atData.metaData = enrollMetaData; } await keyStore.put(enrollmentKey, atData, skipCommit: true); diff --git a/packages/at_secondary_server/pubspec.yaml b/packages/at_secondary_server/pubspec.yaml index a50b558ff..63cade90c 100644 --- a/packages/at_secondary_server/pubspec.yaml +++ b/packages/at_secondary_server/pubspec.yaml @@ -34,7 +34,15 @@ dependencies: yaml: 3.1.2 logging: 1.2.0 +dependency_overrides: + at_commons: + git: + url: https://github.com/atsign-foundation/at_libraries.git + path: packages/at_commons + ref: 2074-introducing-auto-expiry-and-time-to-birth-features-for-apkam-keys + dev_dependencies: + build_runner: ^2.3.3 test: ^1.24.4 coverage: ^1.6.1 lints: ^4.0.0 diff --git a/packages/at_secondary_server/test/enroll_verb_test.dart b/packages/at_secondary_server/test/enroll_verb_test.dart index e6757e90b..f199a92d2 100644 --- a/packages/at_secondary_server/test/enroll_verb_test.dart +++ b/packages/at_secondary_server/test/enroll_verb_test.dart @@ -6,6 +6,7 @@ import 'package:at_persistence_secondary_server/at_persistence_secondary_server. import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; import 'package:at_secondary/src/constants/enroll_constants.dart'; import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; +import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_secondary/src/utils/handler_util.dart'; import 'package:at_secondary/src/verb/handler/delete_verb_handler.dart'; import 'package:at_secondary/src/verb/handler/enroll_verb_handler.dart'; @@ -1014,6 +1015,58 @@ void main() { e.message == 'Failed to revoke enrollment id: $enrollmentId. Cannot revoke a pending enrollment. Only approved enrollments can be revoked'))); }); + + test('A test to verify apkam expiry is set for approved enrollment', + () async { + Response response = Response(); + + inboundConnection.metaData.isAuthenticated = true; + inboundConnection.metaData.sessionID = 'dummy_session'; + // OTP Verb + HashMap otpVerbParams = + getVerbParam(VerbSyntax.otp, 'otp:get'); + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + await otpVerbHandler.processVerb( + response, otpVerbParams, inboundConnection); + + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice"' + ',"namespaces":{"wavi":"r"},"otp":"${response.data}"' + ',"apkamPublicKey":"dummy_apkam_public_key"' + ',"encryptedAPKAMSymmetricKey": "dummy_encrypted_symm_key",' + '"apkamKeysExpiryInMillis":1000}'; + HashMap enrollmentRequestVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.metaData.isAuthenticated = false; + EnrollVerbHandler enrollVerbHandler = + EnrollVerbHandler(secondaryKeyStore); + await enrollVerbHandler.processVerb( + response, enrollmentRequestVerbParams, inboundConnection); + enrollmentId = jsonDecode(response.data!)['enrollmentId']; + expect(jsonDecode(response.data!)['status'], 'pending'); + // Assert the enrollment expiry is set to default value. + AtData? enrollmentAtData = await secondaryKeyStore.get( + '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace$alice'); + expect( + enrollmentAtData?.metaData?.ttl, + Duration(hours: AtSecondaryConfig.enrollmentExpiryInHours) + .inMilliseconds); + + String approveEnrollment = + 'enroll:approve:{"enrollmentId":"$enrollmentId","encryptedDefaultEncryptionPrivateKey":"dummy_encrypted_private_key","encryptedDefaultSelfEncryptionKey":"dummy_self_encrypted_key"}'; + HashMap approveEnrollmentVerbParams = + getVerbParam(VerbSyntax.enroll, approveEnrollment); + inboundConnection.metaData.isAuthenticated = true; + enrollVerbHandler = EnrollVerbHandler(secondaryKeyStore); + await enrollVerbHandler.processVerb( + response, approveEnrollmentVerbParams, inboundConnection); + expect(jsonDecode(response.data!)['status'], 'approved'); + expect(jsonDecode(response.data!)['enrollmentId'], enrollmentId); + + enrollmentAtData = await secondaryKeyStore.get( + '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace$alice'); + expect(enrollmentAtData?.metaData?.ttl, 1000); + }); tearDown(() async => await verbTestsTearDown()); }); From 4ab376c51b3ec220922780eafa05ffb1032cc47b Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Thu, 12 Sep 2024 15:16:09 +0530 Subject: [PATCH 02/11] fix: Use ?? for null check for apkamKeysExpiryDuration --- .../lib/src/enroll/enroll_datastore_value.dart | 2 +- .../lib/src/enroll/enroll_datastore_value.g.dart | 8 ++++---- .../lib/src/verb/handler/enroll_verb_handler.dart | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart index 224a6b7eb..f15033be5 100644 --- a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart +++ b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart @@ -16,7 +16,7 @@ class EnrollDataStoreValue { EnrollRequestType? requestType; EnrollApproval? approval; String? encryptedAPKAMSymmetricKey; - Duration? apkamKeysExpiryDuration; + Duration apkamKeysExpiryDuration = Duration(milliseconds: 0); EnrollDataStoreValue( this.sessionId, this.appName, this.deviceName, this.apkamPublicKey); diff --git a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart index 9ff1d4e93..d61bd8dd6 100644 --- a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart +++ b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart @@ -22,9 +22,8 @@ EnrollDataStoreValue _$EnrollDataStoreValueFromJson( : EnrollApproval.fromJson(json['approval'] as Map) ..encryptedAPKAMSymmetricKey = json['encryptedAPKAMSymmetricKey'] as String? - ..apkamKeysExpiryDuration = (json['apkamExpiryInMillis'] == null) - ? null - : Duration(milliseconds: json['apkamExpiryInMillis']); + ..apkamKeysExpiryDuration = + Duration(milliseconds: json['apkamKeysExpiryInMillis'] ?? 0); Map _$EnrollDataStoreValueToJson( EnrollDataStoreValue instance) => @@ -37,7 +36,8 @@ Map _$EnrollDataStoreValueToJson( 'requestType': _$EnrollRequestTypeEnumMap[instance.requestType], 'approval': instance.approval, 'encryptedAPKAMSymmetricKey': instance.encryptedAPKAMSymmetricKey, - 'apkamExpiryInMillis': instance.apkamKeysExpiryDuration?.inMilliseconds, + 'apkamKeysExpiryInMillis': + instance.apkamKeysExpiryDuration?.inMilliseconds, }; const _$EnrollRequestTypeEnumMap = { diff --git a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart index f37b032c3..cd86b59a7 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart @@ -242,7 +242,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { if (enrollParams.apkamKeysExpiryDuration != null) { enrollmentValue.apkamKeysExpiryDuration = - enrollParams.apkamKeysExpiryDuration; + enrollParams.apkamKeysExpiryDuration!; } AtData enrollData; @@ -607,7 +607,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { // Update key with new data // Update ttl value to support auto expiry of APKAM keys enrollMetaData?.ttl = - enrollDataStoreValue.apkamKeysExpiryDuration?.inMilliseconds; + enrollDataStoreValue.apkamKeysExpiryDuration.inMilliseconds; atData.metaData = enrollMetaData; } await keyStore.put(enrollmentKey, atData, skipCommit: true); From 4c558aca63ea9514e8f56aa8b46ad9d823ce66a3 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Fri, 13 Sep 2024 15:44:54 +0530 Subject: [PATCH 03/11] feat: Update dependent package versions and CHANGELOG.md --- packages/at_secondary_server/CHANGELOG.md | 3 +++ packages/at_secondary_server/pubspec.yaml | 13 +++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/at_secondary_server/CHANGELOG.md b/packages/at_secondary_server/CHANGELOG.md index ae4a57f3f..eb809cab8 100644 --- a/packages/at_secondary_server/CHANGELOG.md +++ b/packages/at_secondary_server/CHANGELOG.md @@ -1,5 +1,7 @@ ## 3.0.50 - fix: Enhance namespace authorisation check to verify when namespace has a period in it +- feat: Enable expiration of APKAM keys based on the specified duration. + ## 3.0.49 - feat: Enforce superset access check for approving apps - fix: respect isEncrypted:false if supplied in the notify: command, and @@ -9,6 +11,7 @@ notifications from the server to the client. - build[deps]: Upgraded the following package: - at_persistence_secondary_server to v3.0.63 + ## 3.0.48 - feat Add expiresAt and availableAt params to notify:list response diff --git a/packages/at_secondary_server/pubspec.yaml b/packages/at_secondary_server/pubspec.yaml index 63cade90c..cb3c27ad6 100644 --- a/packages/at_secondary_server/pubspec.yaml +++ b/packages/at_secondary_server/pubspec.yaml @@ -19,10 +19,10 @@ dependencies: basic_utils: 5.7.0 ecdsa: 0.1.0 encrypt: 5.0.3 - at_commons: 4.0.11 - at_utils: 3.0.16 + at_commons: 4.1.2 + at_utils: 3.0.18 at_chops: 2.0.0 - at_lookup: 3.0.47 + at_lookup: 3.0.48 at_server_spec: 5.0.1 at_persistence_spec: 2.0.14 at_persistence_secondary_server: 3.0.63 @@ -34,13 +34,6 @@ dependencies: yaml: 3.1.2 logging: 1.2.0 -dependency_overrides: - at_commons: - git: - url: https://github.com/atsign-foundation/at_libraries.git - path: packages/at_commons - ref: 2074-introducing-auto-expiry-and-time-to-birth-features-for-apkam-keys - dev_dependencies: build_runner: ^2.3.3 test: ^1.24.4 From bc14389789af60fdc1764864c3386b9f400de5d8 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Fri, 13 Sep 2024 16:14:31 +0530 Subject: [PATCH 04/11] feat: Remove unnecessary null-aware operator --- .../lib/src/enroll/enroll_datastore_value.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart index d61bd8dd6..79db29b92 100644 --- a/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart +++ b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.g.dart @@ -37,7 +37,7 @@ Map _$EnrollDataStoreValueToJson( 'approval': instance.approval, 'encryptedAPKAMSymmetricKey': instance.encryptedAPKAMSymmetricKey, 'apkamKeysExpiryInMillis': - instance.apkamKeysExpiryDuration?.inMilliseconds, + instance.apkamKeysExpiryDuration.inMilliseconds, }; const _$EnrollRequestTypeEnumMap = { From 90dff59a331443d4d939efe055182e7299b7854a Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Wed, 18 Sep 2024 15:23:24 +0530 Subject: [PATCH 05/11] fix: Terminate the inbound connection when apkam keys are expired --- .../verb/handler/abstract_verb_handler.dart | 62 ++++++++++++++++++- .../test/delete_verb_test.dart | 44 +++++++++++++ .../test/pkam_verb_test.dart | 50 ++++++++++++++- .../test/update_verb_test.dart | 52 ++++++++++++++-- .../test/enroll_verb_test.dart | 42 +++++++++++++ 5 files changed, 242 insertions(+), 8 deletions(-) diff --git a/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart index 3bf8a0137..768cbb37e 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart @@ -53,6 +53,16 @@ abstract class AbstractVerbHandler implements VerbHandler { if (getVerb().requiresAuth() && !atConnectionMetadata.isAuthenticated) { throw UnAuthenticatedException('Command cannot be executed without auth'); } + // This check verifies whether the enrollment is active on the already APKAM authenticated existing connection + // and terminates if the enrollment is expired. + // At this stage, the enrollmentId is not set to the InboundConnectionMetadata for the new connections. + // This will not terminate an un-authenticated connection when attempting to execute a PKAM verb with an expired enrollmentId. + (bool, Response) isEnrollmentActive = + await _verifyIfEnrollmentIsActive(response, atConnectionMetadata); + if (isEnrollmentActive.$1 == false) { + await atConnection.close(); + return isEnrollmentActive.$2; + } try { // Parse the command var verbParams = parse(command); @@ -73,6 +83,54 @@ abstract class AbstractVerbHandler implements VerbHandler { } } + /// When authenticated with the APKAM keys, checks if the enrollment is active. + /// Returns true if the enrollment is active; otherwise, returns false. + Future<(bool, Response)> _verifyIfEnrollmentIsActive( + Response response, AtConnectionMetaData atConnectionMetadata) async { + // When authenticated with legacy keys, enrollment id is null. APKAM expiry does not + // apply to such connections. Therefore, return true. + if ((atConnectionMetadata as InboundConnectionMetadata).enrollmentId == + null) { + logger.finest( + "Enrollment id is not found. Returning true from _verifyIfEnrollmentIsActive"); + return (true, response); + } + final enrollmentKey = '${(atConnectionMetadata).enrollmentId}' + '.$newEnrollmentKeyPattern' + '.$enrollManageNamespace' + '${AtSecondaryServerImpl.getInstance().currentAtSign}'; + try { + EnrollDataStoreValue enrollDataStoreValue = + await getEnrollDataStoreValue(enrollmentKey); + // If the enrollment status is expired, then the enrollment is not active. Return false. + if (enrollDataStoreValue.approval?.state == + EnrollmentStatus.expired.name) { + logger.severe( + 'The enrollment id: ${atConnectionMetadata.enrollmentId} is expired. Closing the connection'); + response + ..isError = true + ..errorCode = 'AT0028' + ..errorMessage = + 'The enrollment id: ${(atConnectionMetadata).enrollmentId} is expired. Closing the connection'; + return (false, response); + } + // The expired enrollments are removed from the keystore. In such cases, KeyNotFoundException is + // thrown. Return false. + } on KeyNotFoundException { + logger.severe( + 'The enrollment id: ${atConnectionMetadata.enrollmentId} is expired. Closing the connection'); + response + ..isError = true + ..errorCode = 'AT0028' + ..errorMessage = + 'The enrollment id: ${(atConnectionMetadata).enrollmentId} is expired. Closing the connection'; + return (false, response); + } + logger.finest( + "Enrollment id ${atConnectionMetadata.enrollmentId} is active. Returning true from _verifyIfEnrollmentIsActive"); + return (true, response); + } + /// Return the instance of the current verb ///@return instance of [Verb] Verb getVerb(); @@ -93,9 +151,7 @@ abstract class AbstractVerbHandler implements VerbHandler { AtData enrollData = await keyStore.get(enrollmentKey); EnrollDataStoreValue enrollDataStoreValue = EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!)); - if (!SecondaryUtil.isActiveKey(enrollData) && - enrollDataStoreValue.approval!.state != - EnrollmentStatus.approved.name) { + if (!SecondaryUtil.isActiveKey(enrollData)) { enrollDataStoreValue.approval?.state = EnrollmentStatus.expired.name; } return enrollDataStoreValue; diff --git a/packages/at_secondary_server/test/delete_verb_test.dart b/packages/at_secondary_server/test/delete_verb_test.dart index 33bd38709..2289fa694 100644 --- a/packages/at_secondary_server/test/delete_verb_test.dart +++ b/packages/at_secondary_server/test/delete_verb_test.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:at_commons/at_commons.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; +import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; import 'package:at_secondary/src/notification/stats_notification_service.dart'; import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_secondary/src/utils/handler_util.dart'; @@ -490,4 +491,47 @@ void main() { } tearDown(() async => await verbTestsTearDown()); }); + + group('A group of tests related to apkam keys expiry', () { + Response response = Response(); + late String enrollmentId; + + setUp(() async { + await verbTestsSetUp(); + }); + + tearDown(() async => await verbTestsTearDown()); + + test('A test to verify delete verb fails when apkam keys are expired', + () async { + inboundConnection.metadata.isAuthenticated = + true; // owner connection, authenticated + enrollmentId = Uuid().v4(); + inboundConnection.metadata.enrollmentId = enrollmentId; + EnrollDataStoreValue enrollDataStoreValue = EnrollDataStoreValue( + 'dummy-session', 'app-name', 'my-device', 'dummy-public-key'); + enrollDataStoreValue.namespaces = {'wavi': 'rw'}; + enrollDataStoreValue.approval = + EnrollApproval(EnrollmentStatus.approved.name); + enrollDataStoreValue.apkamKeysExpiryDuration = Duration(milliseconds: 1); + + var keyName = '$enrollmentId.new.enrollments.__manage@alice'; + await secondaryKeyStore.put( + keyName, + AtData() + ..data = jsonEncode(enrollDataStoreValue.toJson()) + ..metaData = (AtMetaData()..ttl = 1)); + + String deleteCommand = 'delete:@alice:phone.wavi@alice'; + + DeleteVerbHandler deleteVerbHandler = + DeleteVerbHandler(secondaryKeyStore, statsNotificationService); + response = await deleteVerbHandler.processInternal( + deleteCommand, inboundConnection); + expect(response.isError, true); + expect(response.errorCode, 'AT0028'); + expect(response.errorMessage, + 'The enrollment id: $enrollmentId is expired. Closing the connection'); + }); + }); } diff --git a/packages/at_secondary_server/test/pkam_verb_test.dart b/packages/at_secondary_server/test/pkam_verb_test.dart index 40143c00a..7948035fd 100644 --- a/packages/at_secondary_server/test/pkam_verb_test.dart +++ b/packages/at_secondary_server/test/pkam_verb_test.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'package:at_commons/at_commons.dart'; @@ -7,8 +8,9 @@ import 'package:at_secondary/src/utils/handler_util.dart'; import 'package:at_secondary/src/utils/secondary_util.dart'; import 'package:at_secondary/src/verb/handler/pkam_verb_handler.dart'; import 'package:at_server_spec/at_verb_spec.dart'; -import 'package:test/test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:uuid/uuid.dart'; import 'test_utils.dart'; @@ -139,4 +141,50 @@ void main() { 'enrollment_id: enrollId is expired or invalid'); }); }); + + group('A group of tests related to apkam keys expiry', () { + Response response = Response(); + late String enrollmentId; + + setUp(() async { + await verbTestsSetUp(); + }); + + tearDown(() async => await verbTestsTearDown()); + + test('A test to verify pkam verb fails when apkam keys are expired', + () async { + inboundConnection.metadata.isAuthenticated = + true; // owner connection, authenticated + enrollmentId = Uuid().v4(); + inboundConnection.metadata.enrollmentId = enrollmentId; + EnrollDataStoreValue enrollDataStoreValue = EnrollDataStoreValue( + 'dummy-session', 'app-name', 'my-device', 'dummy-public-key'); + enrollDataStoreValue.namespaces = {'wavi': 'rw'}; + enrollDataStoreValue.approval = + EnrollApproval(EnrollmentStatus.approved.name); + enrollDataStoreValue.apkamKeysExpiryDuration = Duration(milliseconds: 1); + + var keyName = '$enrollmentId.new.enrollments.__manage@alice'; + await secondaryKeyStore.put( + keyName, + AtData() + ..data = jsonEncode(enrollDataStoreValue.toJson()) + ..metaData = (AtMetaData()..ttl = 1)); + + String pkamCommand = + 'pkam:enrollmentid:$enrollmentId:dummy-pkam-challenge'; + + HashMap pkamVerbParams = + getVerbParam(VerbSyntax.pkam, pkamCommand); + + PkamVerbHandler pkamVerbHandler = PkamVerbHandler(secondaryKeyStore); + await pkamVerbHandler.processVerb( + response, pkamVerbParams, inboundConnection); + expect(response.isError, true); + expect(response.errorCode, 'AT0028'); + expect(response.errorMessage, + 'enrollment_id: $enrollmentId is expired or invalid'); + }); + }); } diff --git a/packages/at_secondary_server/test/update_verb_test.dart b/packages/at_secondary_server/test/update_verb_test.dart index a8c2fb6b2..712af684d 100644 --- a/packages/at_secondary_server/test/update_verb_test.dart +++ b/packages/at_secondary_server/test/update_verb_test.dart @@ -7,6 +7,7 @@ import 'package:at_commons/at_commons.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_impl.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; +import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_secondary/src/utils/handler_util.dart'; @@ -1328,7 +1329,8 @@ void main() { var keyName = '$enrollmentId.new.enrollments.__manage@alice'; await secondaryKeyStore.put( keyName, AtData()..data = jsonEncode(enrollJson)); - String updateCommand = 'update:atconnections.bob.alice.at_contact.buzz$alice bob'; + String updateCommand = + 'update:atconnections.bob.alice.at_contact.buzz$alice bob'; HashMap updateVerbParams = getVerbParam(VerbSyntax.update, updateCommand); UpdateVerbHandler updateVerbHandler = UpdateVerbHandler( @@ -1339,7 +1341,7 @@ void main() { expect(response.isError, false); }); - test( + test( 'A test to verify write access is allowed to a key with a at_contact.buzz namespace for an enrollment with at_contact.buzz namespace access', () async { inboundConnection.metadata.isAuthenticated = @@ -1389,8 +1391,7 @@ void main() { var keyName = '$enrollmentId.new.enrollments.__manage@alice'; await secondaryKeyStore.put( keyName, AtData()..data = jsonEncode(enrollJson)); - String updateCommand = - 'update:atconnections.bob.alice.buzz$alice bob'; + String updateCommand = 'update:atconnections.bob.alice.buzz$alice bob'; HashMap updateVerbParams = getVerbParam(VerbSyntax.update, updateCommand); UpdateVerbHandler updateVerbHandler = UpdateVerbHandler( @@ -1405,4 +1406,47 @@ void main() { }); tearDown(() async => await verbTestsTearDown()); }); + + group('A group of tests related to apkam keys expiry', () { + Response response = Response(); + late String enrollmentId; + + setUp(() async { + await verbTestsSetUp(); + }); + + tearDown(() async => await verbTestsTearDown()); + + test('A test to verify update verb fails when apkam keys are expired', + () async { + inboundConnection.metadata.isAuthenticated = + true; // owner connection, authenticated + enrollmentId = Uuid().v4(); + inboundConnection.metadata.enrollmentId = enrollmentId; + EnrollDataStoreValue enrollDataStoreValue = EnrollDataStoreValue( + 'dummy-session', 'app-name', 'my-device', 'dummy-public-key'); + enrollDataStoreValue.namespaces = {'wavi': 'rw'}; + enrollDataStoreValue.approval = + EnrollApproval(EnrollmentStatus.approved.name); + enrollDataStoreValue.apkamKeysExpiryDuration = Duration(milliseconds: 1); + + var keyName = '$enrollmentId.new.enrollments.__manage@alice'; + await secondaryKeyStore.put( + keyName, + AtData() + ..data = jsonEncode(enrollDataStoreValue.toJson()) + ..metaData = (AtMetaData()..ttl = 1)); + + String updateCommand = 'update:@alice:phone.wavi@alice 123'; + + UpdateVerbHandler updateVerbHandler = UpdateVerbHandler( + secondaryKeyStore, statsNotificationService, notificationManager); + response = await updateVerbHandler.processInternal( + updateCommand, inboundConnection); + expect(response.isError, true); + expect(response.errorCode, 'AT0028'); + expect(response.errorMessage, + 'The enrollment id: $enrollmentId is expired. Closing the connection'); + }); + }); } diff --git a/tests/at_functional_test/test/enroll_verb_test.dart b/tests/at_functional_test/test/enroll_verb_test.dart index 25ea57fef..04010ccc2 100644 --- a/tests/at_functional_test/test/enroll_verb_test.dart +++ b/tests/at_functional_test/test/enroll_verb_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -1275,4 +1276,45 @@ void main() { 'Internal server exception : Failed to revoke enrollment id: $enrollmentId. Client is not authorized for namespaces in the enrollment request'); }); }); + + group('A group of tests related to APKAM keys auto expiry', () { + test('A test to verify apkam authentication fails with expired apkam keys', + () async { + // Fetch OTP. + await firstAtSignConnection.authenticateConnection(); + String otp = await firstAtSignConnection.sendRequestToServer('otp:get'); + otp = otp.replaceAll('data:', ''); + + // Submit an enrollment request from an un-authenticated connection + OutboundConnectionFactory unAuthenticatedConnection = + OutboundConnectionFactory(); + await unAuthenticatedConnection.initiateConnectionWithListener( + firstAtSign, firstAtSignHost, firstAtSignPort); + String enrollmentResponse = + await unAuthenticatedConnection.sendRequestToServer( + 'enroll:request:{"appName":"wavi","deviceName":"pixel-${Uuid().v4().hashCode}","namespaces":{"wavi":"rw","__manage":"rw"},"otp":"$otp","apkamPublicKey":"${apkamPublicKeyMap[firstAtSign]!}","encryptedAPKAMSymmetricKey":"${apkamEncryptedKeysMap['encryptedAPKAMSymmetricKey']}","apkamKeysExpiryInMillis":1}'); + enrollmentResponse = enrollmentResponse.replaceAll('data:', ''); + String enrollmentId = jsonDecode(enrollmentResponse)['enrollmentId']; + expect(jsonDecode(enrollmentResponse)['status'], 'pending'); + + // Approve enrollment with a PKAM Authenticated connection + String approveEnrollmentResponse = + await firstAtSignConnection.sendRequestToServer( + 'enroll:approve:{"enrollmentId":"$enrollmentId","encryptedDefaultEncryptionPrivateKey":"${apkamEncryptedKeysMap['encryptedDefaultEncPrivateKey']}","encryptedDefaultSelfEncryptionKey":"${apkamEncryptedKeysMap['encryptedSelfEncKey']}"}'); + approveEnrollmentResponse = + approveEnrollmentResponse.replaceAll('data:', ''); + expect(jsonDecode(approveEnrollmentResponse)['status'], 'approved'); + + // Perform APKAM authentication with approved enrollment-id + OutboundConnectionFactory enrollmentAuthenticatedConnection = + OutboundConnectionFactory(); + await enrollmentAuthenticatedConnection.initiateConnectionWithListener( + firstAtSign, firstAtSignHost, firstAtSignPort); + String authResponse = + await enrollmentAuthenticatedConnection.authenticateConnection( + authType: AuthType.apkam, enrollmentId: enrollmentId); + expect(authResponse, + 'error:AT0028:enrollment_id: $enrollmentId is expired or invalid'); + }); + }); } From d8ca29ea4749cb9f879c06f30e409ed4aa02b37a Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Thu, 19 Sep 2024 13:55:11 +0530 Subject: [PATCH 06/11] fix: Add functional test to verify connection closes when apkam keys expires --- .../test/enroll_verb_test.dart | 97 ++++++++++++++----- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/tests/at_functional_test/test/enroll_verb_test.dart b/tests/at_functional_test/test/enroll_verb_test.dart index 7df659520..d7be11624 100644 --- a/tests/at_functional_test/test/enroll_verb_test.dart +++ b/tests/at_functional_test/test/enroll_verb_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:at_commons/at_commons.dart'; import 'package:at_demo_data/at_demo_data.dart' as at_demos; import 'package:at_demo_data/at_demo_data.dart'; import 'package:at_functional_test/conf/config_util.dart'; @@ -593,13 +594,13 @@ void main() { enrollmentResponse.replaceAll('data:', ''))['enrollmentId']; //Create a new connection to login using the APKAM - OutboundConnectionFactory socketConnection2 = + OutboundConnectionFactory socketConnection2 = await OutboundConnectionFactory().initiateConnectionWithListener( firstAtSign, firstAtSignHost, firstAtSignPort); - String authResponse = await socketConnection2.authenticateConnection( - authType: AuthType.apkam, enrollmentId: enrollmentId); - expect(authResponse.trim(), 'data:success'); - await socketConnection2.close(); + String authResponse = await socketConnection2.authenticateConnection( + authType: AuthType.apkam, enrollmentId: enrollmentId); + expect(authResponse.trim(), 'data:success'); + await socketConnection2.close(); // Revoke the enrollment String revokeEnrollmentCommand = @@ -626,13 +627,13 @@ void main() { enrollmentResponse.replaceAll('data:', ''))['enrollmentId']; //Create a new connection to login using the APKAM - OutboundConnectionFactory socketConnection2 = + OutboundConnectionFactory socketConnection2 = await OutboundConnectionFactory().initiateConnectionWithListener( firstAtSign, firstAtSignHost, firstAtSignPort); - String authResponse = await socketConnection2.authenticateConnection( - authType: AuthType.apkam, enrollmentId: enrollmentId); - expect(authResponse.trim(), 'data:success'); - await socketConnection2.close(); + String authResponse = await socketConnection2.authenticateConnection( + authType: AuthType.apkam, enrollmentId: enrollmentId); + expect(authResponse.trim(), 'data:success'); + await socketConnection2.close(); // Revoke the enrollment String revokeEnrollmentCommand = @@ -647,14 +648,13 @@ void main() { "Enrollment is revoked. Closing the connection in 10 seconds"); socketConnection2 = await OutboundConnectionFactory() - .initiateConnectionWithListener( + .initiateConnectionWithListener( firstAtSign, firstAtSignHost, firstAtSignPort); - String pkamResult = await socketConnection2.authenticateConnection( - authType: AuthType.apkam, enrollmentId: enrollmentId); - socketConnection2.close(); - assert(pkamResult.contains( - 'enrollment_id: $enrollmentId is revoked')); - }); + String pkamResult = await socketConnection2.authenticateConnection( + authType: AuthType.apkam, enrollmentId: enrollmentId); + await socketConnection2.close(); + assert(pkamResult.contains('enrollment_id: $enrollmentId is revoked')); + }); test( 'A test to verify revoke operation cannot be performed on an unauthenticated connection', @@ -672,12 +672,12 @@ void main() { OutboundConnectionFactory socketConnection2 = await OutboundConnectionFactory().initiateConnectionWithListener( firstAtSign, firstAtSignHost, firstAtSignPort); - String revokeEnrollmentCommand = - 'enroll:revoke:{"enrollmentid":"$enrollmentId"}'; - String revokeEnrollmentResponse = + String revokeEnrollmentCommand = + 'enroll:revoke:{"enrollmentid":"$enrollmentId"}'; + String revokeEnrollmentResponse = await socketConnection2.sendRequestToServer(revokeEnrollmentCommand); - expect(revokeEnrollmentResponse.trim(), - 'error:AT0401-Exception: Cannot revoke enrollment without authentication'); + expect(revokeEnrollmentResponse.trim(), + 'error:AT0401-Exception: Cannot revoke enrollment without authentication'); }); }); @@ -1089,7 +1089,7 @@ void main() { commitIdOfLastEnrolledKey = commitIdOfLastEnrolledKey.replaceAll('data:', '').trim(); // Key which has un-enrolled namespace - firstAtSignConnection.sendRequestToServer( + await firstAtSignConnection.sendRequestToServer( 'update:$secondAtSign:contact-$randomId.atmosphere$firstAtSign random-value'); await firstAtSignConnection.close(); @@ -1444,6 +1444,57 @@ void main() { authType: AuthType.apkam, enrollmentId: enrollmentId); expect(authResponse, 'error:AT0028:enrollment_id: $enrollmentId is expired or invalid'); + + await unAuthenticatedConnection.close(); + await enrollmentAuthenticatedConnection.close(); }); + + test('A test to verify connection closes when apkam keys expire', () async { + // Fetch OTP. + await firstAtSignConnection.authenticateConnection(); + String otp = await firstAtSignConnection.sendRequestToServer('otp:get'); + otp = otp.replaceAll('data:', ''); + + // Submit an enrollment request from an un-authenticated connection + OutboundConnectionFactory unAuthenticatedConnection = + OutboundConnectionFactory(); + await unAuthenticatedConnection.initiateConnectionWithListener( + firstAtSign, firstAtSignHost, firstAtSignPort); + String enrollmentResponse = + await unAuthenticatedConnection.sendRequestToServer( + 'enroll:request:{"appName":"wavi","deviceName":"pixel-${Uuid().v4().hashCode}","namespaces":{"wavi":"rw","__manage":"rw"},"otp":"$otp","apkamPublicKey":"${apkamPublicKeyMap[firstAtSign]!}","encryptedAPKAMSymmetricKey":"${apkamEncryptedKeysMap['encryptedAPKAMSymmetricKey']}","apkamKeysExpiryInMillis":30000}'); + enrollmentResponse = enrollmentResponse.replaceAll('data:', ''); + String enrollmentId = jsonDecode(enrollmentResponse)['enrollmentId']; + expect(jsonDecode(enrollmentResponse)['status'], 'pending'); + await unAuthenticatedConnection.close(); + + // Approve enrollment with a PKAM Authenticated connection + String approveEnrollmentResponse = + await firstAtSignConnection.sendRequestToServer( + 'enroll:approve:{"enrollmentId":"$enrollmentId","encryptedDefaultEncryptionPrivateKey":"${apkamEncryptedKeysMap['encryptedDefaultEncPrivateKey']}","encryptedDefaultSelfEncryptionKey":"${apkamEncryptedKeysMap['encryptedSelfEncKey']}"}'); + approveEnrollmentResponse = + approveEnrollmentResponse.replaceAll('data:', ''); + expect(jsonDecode(approveEnrollmentResponse)['status'], 'approved'); + + // Perform APKAM authentication with approved enrollment-id + OutboundConnectionFactory enrollmentAuthenticatedConnection = + OutboundConnectionFactory(); + await enrollmentAuthenticatedConnection.initiateConnectionWithListener( + firstAtSign, firstAtSignHost, firstAtSignPort); + String authResponse = + await enrollmentAuthenticatedConnection.authenticateConnection( + authType: AuthType.apkam, enrollmentId: enrollmentId); + expect(authResponse, 'data:success'); + + await Future.delayed(Duration(seconds: 30), () async { + await expectLater( + () => enrollmentAuthenticatedConnection.sendRequestToServer('scan'), + throwsA(predicate((dynamic e) => e is AtTimeoutException))); + }); + + await enrollmentAuthenticatedConnection.close(); + // Waits for 1 minute for the APKAM keys to expire. + // A timeout duration of 2 minutes has been added to prevent the test from exiting prematurely. + }, timeout: Timeout(Duration(minutes: 2))); }); } From 1352ef0f0a274c58a70d387a0699400316d02f70 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Thu, 19 Sep 2024 14:21:18 +0530 Subject: [PATCH 07/11] fix: Add future delay to fix the failing unit test --- packages/at_secondary_server/test/update_verb_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/at_secondary_server/test/update_verb_test.dart b/packages/at_secondary_server/test/update_verb_test.dart index 712af684d..681b5b96f 100644 --- a/packages/at_secondary_server/test/update_verb_test.dart +++ b/packages/at_secondary_server/test/update_verb_test.dart @@ -1436,6 +1436,7 @@ void main() { AtData() ..data = jsonEncode(enrollDataStoreValue.toJson()) ..metaData = (AtMetaData()..ttl = 1)); + await Future.delayed(Duration(milliseconds: 2)); String updateCommand = 'update:@alice:phone.wavi@alice 123'; From 8bbc86bd1e80d609a610d17b6c58eacb6998174d Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Mon, 23 Sep 2024 16:19:30 +0530 Subject: [PATCH 08/11] fix: Introduce EnrollmentManager class through which all interaction with enrollment records happens --- .../lib/src/enroll/enrollment_manager.dart | 44 ++++++++++++ .../lib/src/server/at_secondary_impl.dart | 9 ++- .../verb/handler/abstract_verb_handler.dart | 35 ++++------ .../src/verb/handler/enroll_verb_handler.dart | 70 +++++++++---------- .../src/verb/handler/info_verb_handler.dart | 25 ++----- .../src/verb/handler/keys_verb_handler.dart | 24 +++---- .../src/verb/handler/pkam_verb_handler.dart | 13 ++-- .../src/verb/handler/scan_verb_handler.dart | 11 +-- .../src/verb/handler/stats_verb_handler.dart | 8 +-- .../sync_progressive_verb_handler.dart | 8 +-- .../test/enroll_verb_test.dart | 2 +- .../test/notify_verb_test.dart | 7 +- .../test/pkam_verb_test.dart | 4 ++ .../test/sync_unit_test.dart | 3 + .../at_secondary_server/test/test_utils.dart | 10 ++- .../test/info_verb_test.dart | 11 ++- 16 files changed, 163 insertions(+), 121 deletions(-) create mode 100644 packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart diff --git a/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart b/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart new file mode 100644 index 000000000..f4cbeec84 --- /dev/null +++ b/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:at_commons/at_commons.dart'; +import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; +import 'package:at_secondary/src/constants/enroll_constants.dart'; +import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; +import 'package:at_secondary/src/server/at_secondary_impl.dart'; +import 'package:at_secondary/src/utils/secondary_util.dart'; +import 'package:at_utils/at_logger.dart'; + +class EnrollmentManager { + final SecondaryKeyStore _keyStore; + + final logger = AtSignLogger('AtSecondaryServer'); + + EnrollmentManager(this._keyStore); + + Future get(String enrollmentId) async { + String enrollmentKey = buildEnrollmentKey(enrollmentId); + try { + AtData enrollData = await _keyStore.get(enrollmentKey); + EnrollDataStoreValue enrollDataStoreValue = + EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!)); + + if (!SecondaryUtil.isActiveKey(enrollData)) { + enrollDataStoreValue.approval?.state = EnrollmentStatus.expired.name; + } + + return enrollDataStoreValue; + } on KeyNotFoundException { + logger.severe('$enrollmentKey does not exist in the keystore'); + rethrow; + } + } + + String buildEnrollmentKey(String enrollmentId) { + return '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace${AtSecondaryServerImpl.getInstance().currentAtSign}'; + } + + Future put(String enrollmentId, AtData atData) async { + String enrollmentKey = buildEnrollmentKey(enrollmentId); + await _keyStore.put(enrollmentKey, atData, skipCommit: true); + } +} diff --git a/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart b/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart index 30287a9bd..f58b4f9c1 100644 --- a/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart +++ b/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart @@ -7,11 +7,12 @@ import 'dart:math'; import 'package:at_commons/at_commons.dart'; import 'package:at_lookup/at_lookup.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; -import 'package:at_secondary/src/caching/cache_refresh_job.dart'; import 'package:at_secondary/src/caching/cache_manager.dart'; +import 'package:at_secondary/src/caching/cache_refresh_job.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_manager.dart'; import 'package:at_secondary/src/connection/outbound/outbound_client_manager.dart'; import 'package:at_secondary/src/connection/stream_manager.dart'; +import 'package:at_secondary/src/enroll/enrollment_manager.dart'; import 'package:at_secondary/src/exception/global_exception_handler.dart'; import 'package:at_secondary/src/notification/notification_manager_impl.dart'; import 'package:at_secondary/src/notification/queue_manager.dart'; @@ -31,8 +32,8 @@ import 'package:at_server_spec/at_server_spec.dart'; import 'package:at_server_spec/at_verb_spec.dart'; import 'package:at_utils/at_utils.dart'; import 'package:crypton/crypton.dart'; -import 'package:uuid/uuid.dart'; import 'package:meta/meta.dart'; +import 'package:uuid/uuid.dart'; /// [AtSecondaryServerImpl] is a singleton class which implements [AtSecondaryServer] class AtSecondaryServerImpl implements AtSecondaryServer { @@ -106,6 +107,7 @@ class AtSecondaryServerImpl implements AtSecondaryServer { late var atCommitLogCompactionConfig; late var atAccessLogCompactionConfig; late var atNotificationCompactionConfig; + late EnrollmentManager enrollmentManager; @override void setExecutor(VerbExecutor executor) { @@ -169,6 +171,9 @@ class AtSecondaryServerImpl implements AtSecondaryServer { secondaryPersistenceStore = SecondaryPersistenceStoreFactory.getInstance() .getSecondaryPersistenceStore(currentAtSign)!; + // Initialize enrollment manager + enrollmentManager = EnrollmentManager(secondaryKeyStore); + //Commit Log Compaction commitLogCompactionJobInstance = AtCompactionJob(_commitLog, secondaryPersistenceStore); diff --git a/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart index 768cbb37e..c763cbbd0 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart @@ -95,13 +95,11 @@ abstract class AbstractVerbHandler implements VerbHandler { "Enrollment id is not found. Returning true from _verifyIfEnrollmentIsActive"); return (true, response); } - final enrollmentKey = '${(atConnectionMetadata).enrollmentId}' - '.$newEnrollmentKeyPattern' - '.$enrollManageNamespace' - '${AtSecondaryServerImpl.getInstance().currentAtSign}'; try { EnrollDataStoreValue enrollDataStoreValue = - await getEnrollDataStoreValue(enrollmentKey); + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(atConnectionMetadata.enrollmentId!); // If the enrollment status is expired, then the enrollment is not active. Return false. if (enrollDataStoreValue.approval?.state == EnrollmentStatus.expired.name) { @@ -200,21 +198,20 @@ abstract class AbstractVerbHandler implements VerbHandler { return true; } - // Step 1: From the enrollmentId fetch the enrollment details - final enrollmentKey = '${inboundConnectionMetadata.enrollmentId}' - '.$newEnrollmentKeyPattern' - '.$enrollManageNamespace' - '${AtSecondaryServerImpl.getInstance().currentAtSign}'; EnrollDataStoreValue enrollDataStoreValue; + try { - enrollDataStoreValue = await getEnrollDataStoreValue(enrollmentKey); + enrollDataStoreValue = await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(inboundConnectionMetadata.enrollmentId!); } on KeyNotFoundException { - logger.shout('Could not retrieve enrollment data for $enrollmentKey'); + logger.shout( + 'Could not retrieve enrollment data for ${inboundConnectionMetadata.enrollmentId}'); return false; } bool isValid = _applyEnrollmentValidations( - enrollDataStoreValue, enrollmentKey, operation, atKey, namespace); + enrollDataStoreValue, operation, atKey, namespace); if (isValid == false) { return isValid; } @@ -310,18 +307,14 @@ abstract class AbstractVerbHandler implements VerbHandler { return (authorisedNamespace, access); } - bool _applyEnrollmentValidations( - EnrollDataStoreValue enrollDataStoreValue, - String enrollmentKey, - String operation, - String? atKey, - String? namespace) { + bool _applyEnrollmentValidations(EnrollDataStoreValue enrollDataStoreValue, + String operation, String? atKey, String? namespace) { // Only approved enrollmentId is authorised to perform operations. Return false for enrollments // which are not approved. if (enrollDataStoreValue.approval?.state != EnrollmentStatus.approved.name) { - logger.warning('Enrollment state for $enrollmentKey' - ' is ${enrollDataStoreValue.approval?.state}'); + // logger.warning('Enrollment state for $enrollmentKey' + // ' is ${enrollDataStoreValue.approval?.state}'); return false; } // Only the enrollmentId with access to "__manage" namespace can approve, deny, revoke diff --git a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart index 1958e5d41..8f3eebd58 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart @@ -142,23 +142,11 @@ class EnrollVerbHandler extends AbstractVerbHandler { /// Fetches the enrollment request with enrollment id. Future _fetchEnrollmentInfoById( EnrollParams? enrollVerbParams, currentAtSign, Response response) async { - String? enrollmentId = enrollVerbParams?.enrollmentId; - - String enrollmentKey = - '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace$currentAtSign'; - AtData atData; - try { - atData = await keyStore.get(enrollmentKey); - } on KeyNotFoundException { - throw KeyNotFoundException( - 'An Enrollment with Id: ${enrollVerbParams?.enrollmentId} does not exist or has expired.'); - } - if (atData.data == null) { - throw AtEnrollmentException( - 'Enrollment details not found for enrollment id: ${enrollVerbParams?.enrollmentId}'); - } + // Note: The enrollmentId is verified for null check in _validateParams. EnrollDataStoreValue enrollDataStoreValue = - EnrollDataStoreValue.fromJson(jsonDecode(atData.data!)); + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(enrollVerbParams!.enrollmentId!); return jsonEncode({ 'appName': enrollDataStoreValue.appName, 'deviceName': enrollDataStoreValue.deviceName, @@ -228,9 +216,10 @@ class EnrollVerbHandler extends AbstractVerbHandler { var enrollNamespaces = enrollParams.namespaces ?? {}; var newEnrollmentId = Uuid().v4(); - var key = - '$newEnrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace'; - logger.finer('key: $key$currentAtSign'); + var enrollmentKey = AtSecondaryServerImpl.getInstance() + .enrollmentManager + .buildEnrollmentKey(newEnrollmentId); + logger.finer('New enrollment key created : $enrollmentKey$currentAtSign'); responseJson['enrollmentId'] = newEnrollmentId; final enrollmentValue = EnrollDataStoreValue( @@ -269,7 +258,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { enrollmentValue.encryptedAPKAMSymmetricKey = enrollParams.encryptedAPKAMSymmetricKey; enrollmentValue.approval = EnrollApproval(EnrollmentStatus.pending.name); - await _storeNotification(key, enrollParams, currentAtSign); + await _storeNotification(enrollmentKey, enrollParams, currentAtSign); responseJson['status'] = 'pending'; enrollData = AtData() ..data = jsonEncode(enrollmentValue.toJson()) @@ -280,7 +269,10 @@ class EnrollVerbHandler extends AbstractVerbHandler { ..metaData = (AtMetaData()..ttl = enrollmentExpiryInMills); } logger.finer('enrollData: $enrollData'); - await keyStore.put('$key$currentAtSign', enrollData, skipCommit: true); + //await keyStore.put('$key$currentAtSign', enrollData, skipCommit: true); + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .put(newEnrollmentId, enrollData); // Remove the OTP from keystore to prevent reuse. await keyStore.remove( 'private:${enrollParams.otp?.toLowerCase()}${AtSecondaryServerImpl.getInstance().currentAtSign}'); @@ -298,10 +290,6 @@ class EnrollVerbHandler extends AbstractVerbHandler { Map responseJson, Response response) async { final enrollmentIdFromParams = enrollParams.enrollmentId; - String enrollmentKey = - '$enrollmentIdFromParams.$newEnrollmentKeyPattern.$enrollManageNamespace'; - logger.finer( - 'Enrollment key: $enrollmentKey$currentAtSign | Enrollment operation: $operation'); EnrollDataStoreValue? enrollDataStoreValue; EnrollmentStatus? enrollStatus; // Fetch and returns enrollment data from the keystore. @@ -309,8 +297,11 @@ class EnrollVerbHandler extends AbstractVerbHandler { // 1. Enrollment key is not present in keystore // 2. Enrollment key is not active try { - enrollDataStoreValue = - await getEnrollDataStoreValue('$enrollmentKey$currentAtSign'); + // Note: The enrollParams.enrollmentId is verified for null check in _validateParams method. + // Therefore, by the control comes here, enrollmentId will not be null. + enrollDataStoreValue = await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(enrollParams.enrollmentId!); } on KeyNotFoundException { // When an enrollment key is expired or invalid enrollStatus = EnrollmentStatus.expired; @@ -359,7 +350,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { responseJson['status'] = _getEnrollStatusEnum(operation).name; // Update the enrollment status against the enrollment key in keystore. await _updateEnrollmentValueAndResetTTL( - '$enrollmentKey$currentAtSign', enrollDataStoreValue, operation); + enrollParams.enrollmentId!, enrollDataStoreValue, operation); // when enrollment is approved store the apkamPublicKey of the enrollment if (operation == 'approve') { var apkamPublicKeyInKeyStore = @@ -462,10 +453,10 @@ class EnrollVerbHandler extends AbstractVerbHandler { // check if the enrollment has access to __manage namespace. // If enrollApprovalId has access to __manage namespace, return all the enrollments, // Else return only the specific enrollment. - final enrollmentKey = - '$enrollApprovalId.$newEnrollmentKeyPattern.$enrollManageNamespace$currentAtSign'; EnrollDataStoreValue enrollDataStoreValue = - await getEnrollDataStoreValue(enrollmentKey); + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(enrollApprovalId); if (_doesEnrollmentHaveManageNamespace(enrollDataStoreValue)) { await _fetchAllEnrollments(enrollmentKeysList, enrollmentRequestsMap, @@ -473,6 +464,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { } else { if (enrollDataStoreValue.approval!.state != EnrollmentStatus.expired.name) { + String enrollmentKey = AtSecondaryServerImpl.getInstance() + .enrollmentManager + .buildEnrollmentKey(enrollApprovalId); enrollmentRequestsMap[enrollmentKey] = { 'appName': enrollDataStoreValue.appName, 'deviceName': enrollDataStoreValue.deviceName, @@ -530,7 +524,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { notificationValue[AtConstants.namespace] = enrollParams.namespaces; logger.finer('notificationValue:$notificationValue'); final atNotification = (AtNotificationBuilder() - ..notification = '$key$atSign' + ..notification = key ..fromAtSign = atSign ..toAtSign = atSign ..ttl = 24 * 60 * 60 * 1000 @@ -603,7 +597,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { } } - Future _updateEnrollmentValueAndResetTTL(String enrollmentKey, + Future _updateEnrollmentValueAndResetTTL(String enrollmentId, EnrollDataStoreValue enrollDataStoreValue, String operation) async { AtData atData = AtData()..data = jsonEncode(enrollDataStoreValue.toJson()); // If an enrollment is approved, we need the enrollment to be active @@ -611,6 +605,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { // expiredAt on metadata. if (operation == 'approve') { // Fetch the existing data + String enrollmentKey = AtSecondaryServerImpl.getInstance() + .enrollmentManager + .buildEnrollmentKey(enrollmentId); AtMetaData? enrollMetaData = await keyStore.getMeta(enrollmentKey); // Update key with new data // Update ttl value to support auto expiry of APKAM keys @@ -618,7 +615,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { enrollDataStoreValue.apkamKeysExpiryDuration.inMilliseconds; atData.metaData = enrollMetaData; } - await keyStore.put(enrollmentKey, atData, skipCommit: true); + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .put(enrollmentId, atData); } void _validateParams(EnrollParams? enrollParams, String operation, @@ -671,9 +670,10 @@ class EnrollVerbHandler extends AbstractVerbHandler { case 'revoke': case 'deny': case 'unrevoke': + case 'fetch': if (enrollParams!.enrollmentId.isNullOrEmpty) { throw AtEnrollmentException( - 'enrollmentId is mandatory for enroll:revoke/enroll:deny'); + 'enrollmentId is mandatory for revoke,unrevoke, deny and fetch operations of an enrollment'); } break; } diff --git a/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart index 3b4ebe4be..31eaa9aba 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart @@ -2,9 +2,7 @@ import 'dart:collection'; import 'dart:convert'; import 'package:at_commons/at_commons.dart'; -import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; -import 'package:at_secondary/src/constants/enroll_constants.dart'; import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_server_spec/at_server_spec.dart'; @@ -46,14 +44,11 @@ class InfoVerbHandler extends AbstractVerbHandler { if (verbParams[paramFullCommandAsReceived] == 'info') { String uptimeAsWords = durationToWords(uptime); infoMap['uptimeAsWords'] = uptimeAsWords; - final enrollApprovalId = atConnectionMetadata.enrollmentId; - if (atConnectionMetadata.isAuthenticated && enrollApprovalId != null) { - apkamMetadataKey = - '$enrollApprovalId.$newEnrollmentKeyPattern.$enrollManageNamespace$atSign'; - result = await _getApkamMetadataKey(apkamMetadataKey); - if (result != null) { - infoMap['apkam_metadata'] = result; - } + if (atConnectionMetadata.isAuthenticated && + atConnectionMetadata.enrollmentId != null) { + infoMap['apkam_metadata'] = await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(atConnectionMetadata.enrollmentId!); } } else { infoMap['uptimeAsMillis'] = uptime.inMilliseconds; @@ -73,14 +68,4 @@ class InfoVerbHandler extends AbstractVerbHandler { "$uSeconds seconds"; return uptimeAsWords; } - - Future _getApkamMetadataKey(String? apkamMetadataKey) async { - AtData? result; - try { - result = await keyStore.get(apkamMetadataKey); - } on KeyNotFoundException { - logger.warning('apkam key $apkamMetadataKey not found'); - } - return result?.data; - } } diff --git a/packages/at_secondary_server/lib/src/verb/handler/keys_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/keys_verb_handler.dart index 097b03e6a..d23b131d9 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/keys_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/keys_verb_handler.dart @@ -40,19 +40,22 @@ class KeysVerbHandler extends AbstractVerbHandler { 'Keys verb cannot be accessed without an enrollmentId'); } logger.finer('enrollIdFromMetadata:$enrollIdFromMetadata'); - final key = - '$enrollIdFromMetadata.$newEnrollmentKeyPattern.$enrollManageNamespace'; - var enrollData = await _getEnrollData(key, atSign); - if (enrollData != null) { - final enrollDataStoreValue = - EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!)); + try { + EnrollDataStoreValue enrollDataStoreValue = + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(connectionMetadata.enrollmentId!); + if (enrollDataStoreValue.approval?.state != 'approved') { throw AtEnrollmentException( 'Enrollment Id $enrollIdFromMetadata is not approved. current state: ${enrollDataStoreValue.approval?.state}'); } hasManageAccess = enrollDataStoreValue.namespaces[enrollManageNamespace] == 'rw'; + } on KeyNotFoundException { + logger.severe( + 'Enrollment details not found for the enrollmentId: ${connectionMetadata.enrollmentId}'); } final value = verbParams[AtConstants.keyValue]; @@ -78,15 +81,6 @@ class KeysVerbHandler extends AbstractVerbHandler { } } - Future _getEnrollData(String key, String atSign) async { - try { - return await keyStore.get('$key$atSign'); - } on KeyNotFoundException { - logger.warning('enrollment key not found in keystore $key'); - throw AtEnrollmentException('Enrollment Id $key not found in keystore'); - } - } - Future _handlePutOperation( HashMap verbParams, String atSign, diff --git a/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart index 8180c95dd..b82d82f13 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart @@ -5,7 +5,6 @@ import 'dart:typed_data'; import 'package:at_chops/at_chops.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; -import 'package:at_secondary/src/constants/enroll_constants.dart'; import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_secondary/src/verb/handler/abstract_verb_handler.dart'; @@ -82,22 +81,22 @@ class PkamVerbHandler extends AbstractVerbHandler { } @visibleForTesting - Future handleApkamVerification( - String enrollId, String atSign) async { - String enrollmentKey = - '$enrollId.$newEnrollmentKeyPattern.$enrollManageNamespace$atSign'; + Future handleApkamVerification(String enrollmentId, + String atSign) async { late final EnrollDataStoreValue enrollDataStoreValue; ApkamVerificationResult apkamResult = ApkamVerificationResult(); EnrollmentStatus? enrollStatus; try { - enrollDataStoreValue = await getEnrollDataStoreValue(enrollmentKey); + enrollDataStoreValue = await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(enrollmentId); enrollStatus = getEnrollStatusFromString(enrollDataStoreValue.approval!.state); } on KeyNotFoundException catch (e) { logger.finer('Caught exception trying to fetch enrollment key: $e'); enrollStatus = EnrollmentStatus.expired; } - apkamResult.response = _getApprovalStatus(enrollStatus, enrollId); + apkamResult.response = _getApprovalStatus(enrollStatus, enrollmentId); if (apkamResult.response.isError) { return apkamResult; } diff --git a/packages/at_secondary_server/lib/src/verb/handler/scan_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/scan_verb_handler.dart index 2afd4a367..d7751a838 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/scan_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/scan_verb_handler.dart @@ -183,10 +183,13 @@ class ScanVerbHandler extends AbstractVerbHandler { InboundConnectionMetadata atConnectionMetadata, List localKeysList, String currentAtSign) async { - var enrollmentKey = - '${atConnectionMetadata.enrollmentId}.$newEnrollmentKeyPattern.$enrollManageNamespace$currentAtSign'; - var enrollNamespaces = - (await getEnrollDataStoreValue(enrollmentKey)).namespaces; + // NOTE: The atConnectionMetadata.enrollmentId is verified for null check in the caller of this method - getLocalKeys + // Therefore, added non-null assertation operator. + var enrollNamespaces = (await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(atConnectionMetadata.enrollmentId!)) + .namespaces; + // No namespace to filter keys. So, return. if (enrollNamespaces.isEmpty) { logger.finer( diff --git a/packages/at_secondary_server/lib/src/verb/handler/stats_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/stats_verb_handler.dart index 84166f7ac..62851400d 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/stats_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/stats_verb_handler.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'package:at_commons/at_commons.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; -import 'package:at_secondary/src/constants/enroll_constants.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_secondary/src/verb/handler/abstract_verb_handler.dart'; import 'package:at_secondary/src/verb/metrics/metrics_impl.dart'; @@ -157,9 +156,10 @@ class StatsVerbHandler extends AbstractVerbHandler { List enrolledNamespaces = []; if ((atConnection.metaData as InboundConnectionMetadata).enrollmentId != null) { - var enrollmentKey = - '${(atConnection.metaData as InboundConnectionMetadata).enrollmentId}.$newEnrollmentKeyPattern.$enrollManageNamespace${AtSecondaryServerImpl.getInstance().currentAtSign}'; - enrolledNamespaces = (await getEnrollDataStoreValue(enrollmentKey)) + enrolledNamespaces = (await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get((atConnection.metaData as InboundConnectionMetadata) + .enrollmentId!)) .namespaces .keys .toList(); diff --git a/packages/at_secondary_server/lib/src/verb/handler/sync_progressive_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/sync_progressive_verb_handler.dart index c559f7a29..01149568a 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/sync_progressive_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/sync_progressive_verb_handler.dart @@ -64,10 +64,10 @@ class SyncProgressiveVerbHandler extends AbstractVerbHandler { Map enrolledNamespaces = {}; if (enrollmentId != null && enrollmentId.isNotEmpty) { - String enrollmentKey = - '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace${AtSecondaryServerImpl.getInstance().currentAtSign}'; - enrolledNamespaces = - (await getEnrollDataStoreValue(enrollmentKey)).namespaces; + enrolledNamespaces = (await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(enrollmentId)) + .namespaces; } while (commitEntryIterator.moveNext() && diff --git a/packages/at_secondary_server/test/enroll_verb_test.dart b/packages/at_secondary_server/test/enroll_verb_test.dart index f2e4b06d0..5f9f87e11 100644 --- a/packages/at_secondary_server/test/enroll_verb_test.dart +++ b/packages/at_secondary_server/test/enroll_verb_test.dart @@ -1161,7 +1161,7 @@ void main() { throwsA(predicate((dynamic e) => e is AtEnrollmentException && e.message == - 'enrollmentId is mandatory for enroll:revoke/enroll:deny'))); + 'enrollmentId is mandatory for revoke,unrevoke, deny and fetch operations of an enrollment'))); }); test('A test to verify apkam expiry is set for approved enrollment', diff --git a/packages/at_secondary_server/test/notify_verb_test.dart b/packages/at_secondary_server/test/notify_verb_test.dart index 0d427942d..c65fb3e4e 100644 --- a/packages/at_secondary_server/test/notify_verb_test.dart +++ b/packages/at_secondary_server/test/notify_verb_test.dart @@ -8,6 +8,7 @@ import 'package:at_secondary/src/connection/inbound/dummy_inbound_connection.dar import 'package:at_secondary/src/connection/inbound/inbound_connection_impl.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; import 'package:at_secondary/src/connection/outbound/outbound_client_manager.dart'; +import 'package:at_secondary/src/enroll/enrollment_manager.dart'; import 'package:at_secondary/src/notification/at_notification_map.dart'; import 'package:at_secondary/src/notification/queue_manager.dart'; import 'package:at_secondary/src/server/at_secondary_config.dart'; @@ -26,8 +27,8 @@ import 'package:at_secondary/src/verb/handler/notify_verb_handler.dart'; import 'package:at_server_spec/at_verb_spec.dart'; import 'package:crypto/crypto.dart'; import 'package:crypton/crypton.dart'; -import 'package:test/test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; import 'package:uuid/uuid.dart'; import 'test_utils.dart'; @@ -1139,6 +1140,8 @@ void main() { notifyVerbHandler = NotifyVerbHandler(keyStore); notifyAllVerbHandler = NotifyAllVerbHandler(keyStore); inboundConnection = DummyInboundConnection(); + AtSecondaryServerImpl.getInstance().enrollmentManager = + EnrollmentManager(keyStore); registerFallbackValue(inboundConnection); }); test( @@ -1752,6 +1755,8 @@ void main() { notifyListVerbHandler = NotifyListVerbHandler(keyStore, mockOutboundClientManager); inboundConnection = DummyInboundConnection(); + AtSecondaryServerImpl.getInstance().enrollmentManager = + EnrollmentManager(keyStore); registerFallbackValue(inboundConnection); }); test('A test to verify notify:list authorization', () async { diff --git a/packages/at_secondary_server/test/pkam_verb_test.dart b/packages/at_secondary_server/test/pkam_verb_test.dart index 7948035fd..eebe4194b 100644 --- a/packages/at_secondary_server/test/pkam_verb_test.dart +++ b/packages/at_secondary_server/test/pkam_verb_test.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:at_commons/at_commons.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; +import 'package:at_secondary/src/enroll/enrollment_manager.dart'; +import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_secondary/src/utils/handler_util.dart'; import 'package:at_secondary/src/utils/secondary_util.dart'; import 'package:at_secondary/src/verb/handler/pkam_verb_handler.dart'; @@ -72,6 +74,8 @@ void main() { // dummy enroll value enrollData = EnrollDataStoreValue( 'enrollId', 'unit_test', 'test_device', 'dummy_public_key'); + AtSecondaryServerImpl.getInstance().enrollmentManager = + EnrollmentManager(mockKeyStore); pkamVerbHandler = PkamVerbHandler(mockKeyStore); }); diff --git a/packages/at_secondary_server/test/sync_unit_test.dart b/packages/at_secondary_server/test/sync_unit_test.dart index 754ff9c99..ddca654b9 100644 --- a/packages/at_secondary_server/test/sync_unit_test.dart +++ b/packages/at_secondary_server/test/sync_unit_test.dart @@ -9,6 +9,7 @@ import 'package:at_secondary/src/connection/inbound/inbound_connection_impl.dart import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; import 'package:at_secondary/src/connection/outbound/outbound_client_manager.dart'; import 'package:at_secondary/src/constants/enroll_constants.dart'; +import 'package:at_secondary/src/enroll/enrollment_manager.dart'; import 'package:at_secondary/src/notification/notification_manager_impl.dart'; import 'package:at_secondary/src/notification/stats_notification_service.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; @@ -45,6 +46,8 @@ Future setUpMethod() async { .init(storageDir); // Set currentAtSign AtSecondaryServerImpl.getInstance().currentAtSign = atSign; + AtSecondaryServerImpl.getInstance().enrollmentManager = EnrollmentManager( + secondaryPersistenceStore?.getSecondaryKeyStore() as SecondaryKeyStore); } void main() { diff --git a/packages/at_secondary_server/test/test_utils.dart b/packages/at_secondary_server/test/test_utils.dart index f78aa02fb..7e3f45aea 100644 --- a/packages/at_secondary_server/test/test_utils.dart +++ b/packages/at_secondary_server/test/test_utils.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart' as at_lookup; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; import 'package:at_secondary/src/caching/cache_manager.dart'; import 'package:at_secondary/src/connection/inbound/dummy_inbound_connection.dart'; @@ -11,6 +12,7 @@ import 'package:at_secondary/src/connection/inbound/inbound_connection_pool.dart import 'package:at_secondary/src/connection/outbound/outbound_client.dart'; import 'package:at_secondary/src/connection/outbound/outbound_client_manager.dart'; import 'package:at_secondary/src/connection/outbound/outbound_connection.dart'; +import 'package:at_secondary/src/enroll/enrollment_manager.dart'; import 'package:at_secondary/src/notification/notification_manager_impl.dart'; import 'package:at_secondary/src/notification/stats_notification_service.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; @@ -18,7 +20,6 @@ import 'package:at_secondary/src/utils/secondary_util.dart'; import 'package:at_server_spec/at_server_spec.dart'; import 'package:crypton/crypton.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:at_lookup/at_lookup.dart' as at_lookup; class MockSecondaryKeyStore extends Mock implements SecondaryKeyStore {} @@ -47,14 +48,19 @@ class MockSecureSocket extends Mock implements SecureSocket {} class MockSocket extends Mock implements Socket { Completer completer = Completer(); + @override Future get done => completer.future; + @override InternetAddress get remoteAddress => InternetAddress('127.0.0.1'); + @override int get remotePort => 9999; + @override InternetAddress get address => InternetAddress('127.0.0.1'); + @override int get port => 5555; } @@ -200,6 +206,8 @@ verbTestsSetUp() async { AtSecondaryServerImpl.getInstance().currentAtSign = alice; AtSecondaryServerImpl.getInstance().signingKey = bobServerSigningKeypair.privateKey.toString(); + AtSecondaryServerImpl.getInstance().enrollmentManager = + EnrollmentManager(secondaryKeyStore); DateTime now = DateTime.now().toUtcMillisecondsPrecision(); bobOriginalPublicKeyAtData = AtData(); diff --git a/tests/at_functional_test/test/info_verb_test.dart b/tests/at_functional_test/test/info_verb_test.dart index 274c00078..158747449 100644 --- a/tests/at_functional_test/test/info_verb_test.dart +++ b/tests/at_functional_test/test/info_verb_test.dart @@ -58,12 +58,11 @@ void main() { Map infoResponse = jsonDecode(infoVerbResponse); print('infoResponse: $enrollResponse'); expect(infoResponse['apkam_metadata'], isNotEmpty); - var apkamMetadata = jsonDecode(infoResponse['apkam_metadata']); // Assert the APKAM metadata - expect(apkamMetadata['appName'], 'wavi-$random'); - expect(apkamMetadata['deviceName'], 'pixel-$random'); - expect(apkamMetadata['namespaces'], {"wavi": "rw"}); - expect(apkamMetadata['sessionId'], isNotNull); - expect(apkamMetadata['apkamPublicKey'], isNotNull); + expect(infoResponse['apkam_metadata']['appName'], 'wavi-$random'); + expect(infoResponse['apkam_metadata']['deviceName'], 'pixel-$random'); + expect(infoResponse['apkam_metadata']['namespaces'], {"wavi": "rw"}); + expect(infoResponse['apkam_metadata']['sessionId'], isNotNull); + expect(infoResponse['apkam_metadata']['apkamPublicKey'], isNotNull); }); } From d6a0f4a94443af8d8f3e65df29c6c687de9b1d7a Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Mon, 23 Sep 2024 16:36:36 +0530 Subject: [PATCH 09/11] fix: Remove un-used variables from info_verb_handler.dart --- .../lib/src/verb/handler/info_verb_handler.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart index 31eaa9aba..a16811a04 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart @@ -31,11 +31,8 @@ class InfoVerbHandler extends AbstractVerbHandler { HashMap verbParams, InboundConnection atConnection) async { Map infoMap = {}; - String? apkamMetadataKey; - String? result; InboundConnectionMetadata atConnectionMetadata = atConnection.metaData as InboundConnectionMetadata; // structure of what is returned is documented in the [Info] verb in at_server_spec - var atSign = AtSecondaryServerImpl.getInstance().currentAtSign; infoMap['version'] = AtSecondaryConfig.secondaryServerVersion; Duration uptime = Duration( From 7f9ff68c755be7d05d4d6a3636c0ff641dda5a59 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Tue, 24 Sep 2024 22:18:20 +0530 Subject: [PATCH 10/11] fix: Include EnrollmentManager changes into enrollment delete operation changes --- .../src/verb/handler/enroll_verb_handler.dart | 21 ++++++---- .../test/enroll_verb_test.dart | 42 ++++++++++++------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart index 211b86a9f..2ce0b8f65 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart @@ -301,7 +301,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { // 2. Enrollment key is not active try { // Note: The enrollParams.enrollmentId is verified for null check in _validateParams method. - // Therefore, by the control comes here, enrollmentId will not be null. + // Therefore, when control comes here, enrollmentId will not be null. enrollDataStoreValue = await AtSecondaryServerImpl.getInstance() .enrollmentManager .get(enrollParams.enrollmentId!); @@ -735,10 +735,12 @@ class EnrollVerbHandler extends AbstractVerbHandler { } Future _deleteDeniedEnrollment(EnrollParams? enrollParams, - String atsign, Map responseJson, response) async { - String deleteKey = - '${enrollParams!.enrollmentId}.$newEnrollmentKeyPattern.$enrollManageNamespace$atsign'; - EnrollDataStoreValue enrollValue = await getEnrollDataStoreValue(deleteKey); + String atSign, Map responseJson, response) async { + // Note: The enrollmentId is verified for the null check in the _validateParams methods. + // Therefore, when control comes here, enrollmentId will not be null. + EnrollDataStoreValue enrollValue = await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .get(enrollParams!.enrollmentId!); EnrollmentStatus enrollmentStatus = getEnrollStatusFromString(enrollValue.approval!.state); if (EnrollmentStatus.expired == enrollmentStatus) { @@ -746,10 +748,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { response.errorCode = 'AT0028'; response.errorMessage = 'enrollment_id: ${enrollParams.enrollmentId} is expired or invalid'; - } - if (response.isError) { return; } + // ensures only denied entries can be deleted try { _verifyEnrollmentStateBeforeAction( @@ -759,7 +760,11 @@ class EnrollVerbHandler extends AbstractVerbHandler { 'Failed to delete enrollment id: ${enrollParams.enrollmentId} | Cause: ${e.message}'); } - await keyStore.remove(deleteKey); + String enrollmentKeyToDelete = AtSecondaryServerImpl.getInstance() + .enrollmentManager + .buildEnrollmentKey(enrollParams.enrollmentId!); + await keyStore.remove(enrollmentKeyToDelete); + responseJson['enrollmentId'] = enrollParams.enrollmentId; responseJson['status'] = 'deleted'; } diff --git a/packages/at_secondary_server/test/enroll_verb_test.dart b/packages/at_secondary_server/test/enroll_verb_test.dart index f48f1ddd2..dacb1276d 100644 --- a/packages/at_secondary_server/test/enroll_verb_test.dart +++ b/packages/at_secondary_server/test/enroll_verb_test.dart @@ -1160,8 +1160,7 @@ void main() { response, enrollVerbParams, inboundConnection), throwsA(predicate((dynamic e) => e is AtEnrollmentException && - e.message == - 'enrollmentId is mandatory for revoke,unrevoke, deny and fetch operations of an enrollment'))); + e.message == 'enrollmentId is mandatory for enroll:unrevoke'))); }); test('A test to verify apkam expiry is set for approved enrollment', @@ -1894,6 +1893,8 @@ void main() { }); group('Group of tests to validate enroll delete operation', () { + Response response = Response(); + setUp(() async { await verbTestsSetUp(); }); @@ -1914,11 +1915,13 @@ void main() { castMetadata(inboundConnection).enrollmentId = '123'; String enrollDeleteCommand = 'enroll:delete:{"enrollmentId":"$dummyEnrollId"}'; + EnrollVerbHandler enrollVerb = EnrollVerbHandler(secondaryKeyStore); + var enrollVerbParams = enrollVerb.parse(enrollDeleteCommand); - Response verbResponse = await enrollVerb.processInternal( - enrollDeleteCommand, inboundConnection); - expect(verbResponse.data, + await enrollVerb.processVerb( + response, enrollVerbParams, inboundConnection); + expect(response.data, '{"enrollmentId":"$dummyEnrollId","status":"deleted"}'); }); @@ -1938,11 +1941,13 @@ void main() { castMetadata(inboundConnection).enrollmentId = '123'; String enrollDeleteCommand = 'enroll:delete:{"enrollmentId":"$dummyEnrollId"}'; + EnrollVerbHandler enrollVerb = EnrollVerbHandler(secondaryKeyStore); + var enrollVerbParams = enrollVerb.parse(enrollDeleteCommand); - Response verbResponse = await enrollVerb.processInternal( - enrollDeleteCommand, inboundConnection); - expect(verbResponse.data, + await enrollVerb.processVerb( + response, enrollVerbParams, inboundConnection); + expect(response.data, '{"enrollmentId":"$dummyEnrollId","status":"deleted"}'); }); @@ -1964,11 +1969,13 @@ void main() { castMetadata(inboundConnection).enrollmentId = '123653'; String enrollDeleteCommand = 'enroll:delete:{"enrollmentId":"$dummyEnrollId"}'; + EnrollVerbHandler enrollVerb = EnrollVerbHandler(secondaryKeyStore); + var enrollVerbParams = enrollVerb.parse(enrollDeleteCommand); expect( - () => enrollVerb.processInternal( - enrollDeleteCommand, inboundConnection), + () async => await enrollVerb.processVerb( + response, enrollVerbParams, inboundConnection), throwsA(predicate((e) => e.toString() == 'Exception: Cannot delete enrollment without authentication'))); @@ -1992,11 +1999,13 @@ void main() { castMetadata(inboundConnection).enrollmentId = '1425365'; String enrollDeleteCommand = 'enroll:delete:{"enrollmentId":"$dummyEnrollId"}'; + EnrollVerbHandler enrollVerb = EnrollVerbHandler(secondaryKeyStore); + var enrollVerbParams = enrollVerb.parse(enrollDeleteCommand); expect( - () => enrollVerb.processInternal( - enrollDeleteCommand, inboundConnection), + () => enrollVerb.processVerb( + response, enrollVerbParams, inboundConnection), throwsA(predicate((e) => e.toString() == 'Exception: Cannot delete enrollment without authentication'))); @@ -2019,15 +2028,16 @@ void main() { String enrollDeleteCommand = 'enroll:delete:{"enrollmentId":"$dummyEnrollId"}'; - EnrollVerbHandler enrollVerb = EnrollVerbHandler(secondaryKeyStore); + EnrollVerbHandler enrollVerbHandler = + EnrollVerbHandler(secondaryKeyStore); + var enrollVerbParams = enrollVerbHandler.parse(enrollDeleteCommand); expect( - () => enrollVerb.processInternal( - enrollDeleteCommand, inboundConnection), + () => enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection), throwsA(predicate((e) => e.toString() == 'Exception: Failed to delete enrollment id: 345345345141 | Cause: Cannot delete approved enrollments. Only denied enrollments can be deleted'))); }); - tearDown(() async => await verbTestsTearDown()); }); } From a0fe5a7a90a4a4b0340d046ab1e653fc6c9e7421 Mon Sep 17 00:00:00 2001 From: Sitaram Kalluri Date: Tue, 24 Sep 2024 22:32:20 +0530 Subject: [PATCH 11/11] fix: Add dart docs for the enrollment_manager.dart and remove commented code in enroll_verb_handler.dart --- .../lib/src/enroll/enrollment_manager.dart | 38 +++++++++++++++++++ .../src/verb/handler/enroll_verb_handler.dart | 1 - 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart b/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart index f4cbeec84..bdca3dc27 100644 --- a/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart +++ b/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart @@ -8,13 +8,35 @@ import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_secondary/src/utils/secondary_util.dart'; import 'package:at_utils/at_logger.dart'; +/// Manages enrollment data in the secondary server. +/// +/// This class provides methods to retrieve and store enrollment data +/// associated with a given enrollment ID. It interacts with the +/// SecondaryKeyStore to persist and retrieve enrollment information. class EnrollmentManager { final SecondaryKeyStore _keyStore; final logger = AtSignLogger('AtSecondaryServer'); + /// Creates an instance of [EnrollmentManager]. + /// + /// The [keyStore] is required to interact with the persistence layer. EnrollmentManager(this._keyStore); + /// Retrieves the enrollment data for a given [enrollmentId]. + /// + /// This method constructs an enrollment key, fetches the corresponding + /// data from the key store, and returns it as an [EnrollDataStoreValue]. + /// If the key is not found, a [KeyNotFoundException] is thrown. + /// + /// If the retrieved enrollment data is no longer active, the status + /// will be set to `expired`. + /// + /// Returns: + /// An [EnrollDataStoreValue] containing the enrollment details. + /// + /// Throws: + /// [KeyNotFoundException] if the enrollment key does not exist. Future get(String enrollmentId) async { String enrollmentKey = buildEnrollmentKey(enrollmentId); try { @@ -33,10 +55,26 @@ class EnrollmentManager { } } + /// Constructs the enrollment key based on the provided [enrollmentId]. + /// + /// The key format combines the [enrollmentId], a new enrollment key pattern, + /// and the current AtSign. + /// + /// Returns: + /// A [String] representing the enrollment key. String buildEnrollmentKey(String enrollmentId) { return '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace${AtSecondaryServerImpl.getInstance().currentAtSign}'; } + /// Stores the enrollment data associated with the given [enrollmentId]. + /// + /// This method constructs an enrollment key and saves the provided [AtData] + /// to the key store. The skipCommit is set to true, to prevent the enrollment + /// data being synced to the client(s). + /// + /// Parameters: + /// - [enrollmentId]: The ID associated with the enrollment. + /// - [atData]: The [AtData] object to be stored. Future put(String enrollmentId, AtData atData) async { String enrollmentKey = buildEnrollmentKey(enrollmentId); await _keyStore.put(enrollmentKey, atData, skipCommit: true); diff --git a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart index 2ce0b8f65..d9dad5171 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart @@ -272,7 +272,6 @@ class EnrollVerbHandler extends AbstractVerbHandler { ..metaData = (AtMetaData()..ttl = enrollmentExpiryInMills); } logger.finer('enrollData: $enrollData'); - //await keyStore.put('$key$currentAtSign', enrollData, skipCommit: true); await AtSecondaryServerImpl.getInstance() .enrollmentManager .put(newEnrollmentId, enrollData);