diff --git a/packages/at_secondary_server/CHANGELOG.md b/packages/at_secondary_server/CHANGELOG.md index 8799047a6..fec02a7ed 100644 --- a/packages/at_secondary_server/CHANGELOG.md +++ b/packages/at_secondary_server/CHANGELOG.md @@ -5,6 +5,8 @@ - fix: LatestCommitEntryOfEachKey metric fixed to return commit log entries till last commitID instead of default limit 25. ## 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 @@ -14,6 +16,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/lib/src/enroll/enroll_datastore_value.dart b/packages/at_secondary_server/lib/src/enroll/enroll_datastore_value.dart index 2e964b2b5..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,6 +16,7 @@ class EnrollDataStoreValue { EnrollRequestType? requestType; EnrollApproval? approval; String? encryptedAPKAMSymmetricKey; + 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 80e348b57..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 @@ -21,7 +21,9 @@ EnrollDataStoreValue _$EnrollDataStoreValueFromJson( ? null : EnrollApproval.fromJson(json['approval'] as Map) ..encryptedAPKAMSymmetricKey = - json['encryptedAPKAMSymmetricKey'] as String?; + json['encryptedAPKAMSymmetricKey'] as String? + ..apkamKeysExpiryDuration = + Duration(milliseconds: json['apkamKeysExpiryInMillis'] ?? 0); Map _$EnrollDataStoreValueToJson( EnrollDataStoreValue instance) => @@ -34,6 +36,8 @@ Map _$EnrollDataStoreValueToJson( 'requestType': _$EnrollRequestTypeEnumMap[instance.requestType], 'approval': instance.approval, 'encryptedAPKAMSymmetricKey': instance.encryptedAPKAMSymmetricKey, + 'apkamKeysExpiryInMillis': + instance.apkamKeysExpiryDuration.inMilliseconds, }; const _$EnrollRequestTypeEnumMap = { 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..bdca3dc27 --- /dev/null +++ b/packages/at_secondary_server/lib/src/enroll/enrollment_manager.dart @@ -0,0 +1,82 @@ +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'; + +/// 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 { + 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; + } + } + + /// 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/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 19282be95..685341028 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,52 @@ 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); + } + try { + EnrollDataStoreValue enrollDataStoreValue = + 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) { + 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 +149,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; @@ -144,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) { return isValid; } @@ -254,18 +307,12 @@ 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}'); 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 a708bd85f..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 @@ -63,7 +63,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { logger.finer('verb params: $verbParams'); final operation = verbParams['operation']; final currentAtSign = AtSecondaryServerImpl.getInstance().currentAtSign; - // //Approve, deny, revoke or list enrollments only on authenticated connections + // Approve, deny, revoke or list enrollments only on authenticated connections if (operation != 'request' && !atConnection.metaData.isAuthenticated) { throw UnAuthenticatedException( 'Cannot $operation enrollment without authentication'); @@ -145,23 +145,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, @@ -231,9 +219,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( @@ -243,6 +232,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) { @@ -266,7 +261,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()) @@ -277,7 +272,9 @@ 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); // Remove the OTP from keystore to prevent reuse. await keyStore.remove( 'private:${enrollParams.otp?.toLowerCase()}${AtSecondaryServerImpl.getInstance().currentAtSign}'); @@ -295,10 +292,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. @@ -306,8 +299,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, when 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; @@ -356,7 +352,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 = @@ -459,10 +455,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, @@ -470,6 +466,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, @@ -527,7 +526,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 @@ -606,23 +605,27 @@ 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 // to subsequently revoke the enrollment. Hence reset TTL and // expiredAt on metadata. - ///ToDo: should unrevoke be added in the below condition 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 - // 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); + await AtSecondaryServerImpl.getInstance() + .enrollmentManager + .put(enrollmentId, atData); } void _validateParams(EnrollParams? enrollParams, String operation, @@ -676,6 +679,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { case 'deny': case 'delete': case 'unrevoke': + case 'fetch': if (enrollParams!.enrollmentId.isNullOrEmpty) { throw AtEnrollmentException( 'enrollmentId is mandatory for enroll:$operation'); @@ -730,10 +734,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) { @@ -741,10 +747,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( @@ -754,7 +759,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/lib/src/verb/handler/info_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/info_verb_handler.dart index 3b4ebe4be..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 @@ -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'; @@ -33,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( @@ -46,14 +41,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 +65,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/pubspec.yaml b/packages/at_secondary_server/pubspec.yaml index 2a590f076..7a497aafb 100644 --- a/packages/at_secondary_server/pubspec.yaml +++ b/packages/at_secondary_server/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: logging: 1.2.0 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/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/enroll_verb_test.dart b/packages/at_secondary_server/test/enroll_verb_test.dart index f7021e17c..dacb1276d 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'; @@ -127,7 +128,6 @@ void main() { }); tearDown(() async => await verbTestsTearDown()); }); - group('A group of tests to verify enroll list operation', () { setUp(() async { await verbTestsSetUp(); @@ -1162,6 +1162,58 @@ void main() { e is AtEnrollmentException && e.message == 'enrollmentId is mandatory for enroll:unrevoke'))); }); + + 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()); }); @@ -1841,6 +1893,8 @@ void main() { }); group('Group of tests to validate enroll delete operation', () { + Response response = Response(); + setUp(() async { await verbTestsSetUp(); }); @@ -1861,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"}'); }); @@ -1885,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"}'); }); @@ -1911,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'))); @@ -1939,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'))); @@ -1966,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()); }); } 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 40143c00a..eebe4194b 100644 --- a/packages/at_secondary_server/test/pkam_verb_test.dart +++ b/packages/at_secondary_server/test/pkam_verb_test.dart @@ -1,14 +1,18 @@ +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/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'; 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'; @@ -70,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); }); @@ -139,4 +145,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/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/packages/at_secondary_server/test/update_verb_test.dart b/packages/at_secondary_server/test/update_verb_test.dart index a8c2fb6b2..681b5b96f 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,48 @@ 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)); + await Future.delayed(Duration(milliseconds: 2)); + + 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 5edfaf8a8..19ebbdb5a 100644 --- a/tests/at_functional_test/test/enroll_verb_test.dart +++ b/tests/at_functional_test/test/enroll_verb_test.dart @@ -1,6 +1,7 @@ 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'; @@ -592,13 +593,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 = @@ -625,13 +626,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 = @@ -646,11 +647,11 @@ 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(); + await socketConnection2.close(); assert(pkamResult.contains('enrollment_id: $enrollmentId is revoked')); }); @@ -670,12 +671,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'); }); }); @@ -1087,7 +1088,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(); @@ -1404,6 +1405,98 @@ void main() { }); }); + 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'); + + 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))); + }); + group('tests to validate enroll delete', () { test('delete an denied enrollment', () async { // Send an enrollment request on the authenticated connection 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); }); }