diff --git a/.github/workflows/at_server.yaml b/.github/workflows/at_server.yaml index 88b1d75e1..c4d57f30b 100644 --- a/.github/workflows/at_server.yaml +++ b/.github/workflows/at_server.yaml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d # v1.6.0 with: sdk: stable @@ -129,7 +129,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d # v1.6.0 with: sdk: stable @@ -229,7 +229,7 @@ jobs: steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Place run number into version within pubspec.yaml working-directory: ${{ env.secondary-working-directory }} @@ -281,7 +281,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d # v 1.6.0 with: sdk: stable @@ -310,7 +310,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d # v1.6.0 with: sdk: stable @@ -339,7 +339,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d # v1.6.0 with: sdk: stable @@ -367,7 +367,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout at_server repo - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Create atSigns id: atsign_names @@ -402,14 +402,14 @@ jobs: run: dart pub get - name: Cloning at_libraries - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: repository: atsign-foundation/at_libraries path: at_libraries ref: trunk - name: Cloning at_tools - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: repository: atsign-foundation/at_tools path: at_tools @@ -581,7 +581,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Place run number into version within pubspec.yaml working-directory: ${{ env.secondary-working-directory }} @@ -635,7 +635,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Place run number into version within pubspec.yaml working-directory: ${{ env.secondary-working-directory }} @@ -690,7 +690,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Place run number into version within pubspec.yaml working-directory: ${{ env.secondary-working-directory }} @@ -743,7 +743,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract version for docker tag - name: Get version @@ -789,7 +789,7 @@ jobs: if: ${{ github.repository == 'atsign-foundation/at_server' && github.event_name == 'push' && contains(github.ref, 'refs/tags/c') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Get version run: echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV @@ -840,7 +840,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract version for docker tag - name: Get version diff --git a/.github/workflows/at_server_dev_deploy.yaml b/.github/workflows/at_server_dev_deploy.yaml index a98b9bada..451e43b49 100644 --- a/.github/workflows/at_server_dev_deploy.yaml +++ b/.github/workflows/at_server_dev_deploy.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract branch for docker tag - name: Get branch name @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract branch for docker tag - name: Get branch name run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV diff --git a/.github/workflows/at_server_prod_deploy.yaml b/.github/workflows/at_server_prod_deploy.yaml index 5aa21f72c..187b486a1 100644 --- a/.github/workflows/at_server_prod_deploy.yaml +++ b/.github/workflows/at_server_prod_deploy.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract branch for docker tag - name: Get branch name @@ -45,7 +45,7 @@ jobs: runs-on: [self-hosted, linux, x64, K8s] steps: - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract branch for docker tag - name: Get branch name run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8d9f7dcbb..14b382864 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index dc49c09bc..104d2718a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,6 +22,6 @@ jobs: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 diff --git a/.github/workflows/melos_bootstrap.yaml b/.github/workflows/melos_bootstrap.yaml index 613f2c090..d97abaab3 100644 --- a/.github/workflows/melos_bootstrap.yaml +++ b/.github/workflows/melos_bootstrap.yaml @@ -18,7 +18,7 @@ jobs: melos-bootstrap: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: subosito/flutter-action@cc97e1648fff6ca5cc647fa67f47e70f7895510b # v2.11.0 with: channel: "stable" diff --git a/.github/workflows/promote_canary.yaml b/.github/workflows/promote_canary.yaml index bae27f914..5edd61e45 100644 --- a/.github/workflows/promote_canary.yaml +++ b/.github/workflows/promote_canary.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract version for docker tag - name: Get version @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract version for docker tag - name: Get version diff --git a/.github/workflows/refreshcerts.yaml b/.github/workflows/refreshcerts.yaml index 5f919ea2f..3fde656fd 100644 --- a/.github/workflows/refreshcerts.yaml +++ b/.github/workflows/refreshcerts.yaml @@ -20,10 +20,10 @@ jobs: uses: atsign-company/certinfo-action@e33db584f27bbbc0260af9916aeaefbec0db8ef4 # v1.0.1 # checkout at_server code - name: checkout repo content - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Pull ACME script - name: Pull ACME script - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: repository: atsign-company/secondaries-scripts path: secondaries-scripts diff --git a/.github/workflows/revert_secondary.yaml b/.github/workflows/revert_secondary.yaml index 1582864aa..d5bafcf3f 100644 --- a/.github/workflows/revert_secondary.yaml +++ b/.github/workflows/revert_secondary.yaml @@ -22,7 +22,7 @@ jobs: if: ${{ github.event.inputs.rollback_prod_secondary_image == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract version for docker tag - name: Get version @@ -65,7 +65,7 @@ jobs: if: ${{ github.event.inputs.rollback_canary_secondary_image == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Extract version for docker tag - name: Get version diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 9621f66a3..398e744c7 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false diff --git a/.github/workflows/update_python_requirements.yml b/.github/workflows/update_python_requirements.yml index 8b8f865d5..5ec545320 100644 --- a/.github/workflows/update_python_requirements.yml +++ b/.github/workflows/update_python_requirements.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout this repo if: ${{ github.actor == 'dependabot[bot]' }} - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} diff --git a/packages/at_secondary_server/CHANGELOG.md b/packages/at_secondary_server/CHANGELOG.md index 4a5017e56..79d1e39dc 100644 --- a/packages/at_secondary_server/CHANGELOG.md +++ b/packages/at_secondary_server/CHANGELOG.md @@ -2,6 +2,8 @@ - fix: Implement notify ephemeral changes - Send notification with value without caching the key on receiver's secondary server - feat: Implement AtRateLimiter to limit the enrollment requests on a particular connection - fix: Upgraded at_commons to 3.0.56 +- fix: Enable client to set OTP expiry via OTP verb +- fix: Prevent reuse of OTP - fix: Modify sync_progressive_verb_handler to filter responses on enrolled namespaces if authenticated via APKAM ## 3.0.35 - chore: Upgraded at_persistence_secondary_server to 3.0.57 for memory optimization in commit log 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 5e0c52b50..e39f9f05d 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 @@ -152,4 +152,25 @@ abstract class AbstractVerbHandler implements VerbHandler { return false; } } + + + /// This function checks the validity of a provided OTP. + /// It returns true if the OTP is valid; otherwise, it returns false. + /// If the OTP is not found in the keystore, it also returns false. + /// + /// Additionally, this function removes the OTP from the keystore to prevent its reuse. + Future isOTPValid(String? otp) async { + if (otp == null) { + return false; + } + String otpKey = + 'private:${otp.toLowerCase()}${AtSecondaryServerImpl.getInstance().currentAtSign}'; + AtData otpAtData; + try { + otpAtData = await keyStore.get(otpKey); + } on KeyNotFoundException { + return false; + } + return SecondaryUtil.isActiveKey(otpAtData); + } } 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 31b43f930..215f8104c 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 @@ -8,7 +8,6 @@ import 'package:at_secondary/src/server/at_secondary_config.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/utils/notification_util.dart'; -import 'package:at_secondary/src/verb/handler/otp_verb_handler.dart'; import 'package:at_server_spec/at_server_spec.dart'; import 'package:at_server_spec/at_verb_spec.dart'; import 'package:meta/meta.dart'; @@ -50,15 +49,15 @@ class EnrollVerbHandler extends AbstractVerbHandler { try { // Ensure that enrollParams are present for all enroll operation // Exclude operation 'list' which does not have enrollParams - if (verbParams[enrollParams] == null) { + if (verbParams[AtConstants.enrollParams] == null) { if (operation != 'list') { logger.severe( - 'Enroll params is empty | EnrollParams: ${verbParams[enrollParams]}'); + 'Enroll params is empty | EnrollParams: ${verbParams[AtConstants.enrollParams]}'); throw IllegalArgumentException('Enroll parameters not provided'); } } else { - enrollVerbParams = - EnrollParams.fromJson(jsonDecode(verbParams[enrollParams]!)); + enrollVerbParams = EnrollParams.fromJson( + jsonDecode(verbParams[AtConstants.enrollParams]!)); } switch (operation) { case 'request': @@ -116,14 +115,17 @@ class EnrollVerbHandler extends AbstractVerbHandler { throw AtThrottleLimitExceeded( 'Enrollment requests have exceeded the limit within the specified time frame'); } - if (!atConnection.getMetaData().isAuthenticated) { - var otp = enrollParams.otp; - if (otp == null || - (await OtpVerbHandler.cache.get(otp.toString()) == null)) { + + // OTP is sent only in enrollment request which is submitted on + // unauthenticated connection. + if (atConnection.getMetaData().isAuthenticated == false) { + var isValid = await isOTPValid(enrollParams.otp); + if (!isValid) { throw AtEnrollmentException( 'invalid otp. Cannot process enroll request'); } } + var enrollNamespaces = enrollParams.namespaces ?? {}; var newEnrollmentId = Uuid().v4(); var key = @@ -154,8 +156,8 @@ class EnrollVerbHandler extends AbstractVerbHandler { await _storeEncryptionKeys(newEnrollmentId, enrollParams, currentAtSign); // store this apkam as default pkam public key for old clients // The keys with AT_PKAM_PUBLIC_KEY does not sync to client. - await keyStore.put( - AT_PKAM_PUBLIC_KEY, AtData()..data = enrollParams.apkamPublicKey!); + await keyStore.put(AtConstants.atPkamPublicKey, + AtData()..data = enrollParams.apkamPublicKey!); enrollData = AtData()..data = jsonEncode(enrollmentValue.toJson()); } else { enrollmentValue.approval = EnrollApproval(EnrollStatus.pending.name); @@ -171,6 +173,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { } logger.finer('enrollData: $enrollData'); await keyStore.put('$key$currentAtSign', enrollData, skipCommit: true); + // Remove the OTP from keystore to prevent reuse. + await keyStore.remove( + 'private:${enrollParams.otp?.toLowerCase()}${AtSecondaryServerImpl.getInstance().currentAtSign}'); } /// Handles enrollment approve, deny and revoke requests. @@ -224,10 +229,6 @@ class EnrollVerbHandler extends AbstractVerbHandler { // 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: Currently TTL is reset on all the enrollments. - However, if the enrollment state is denied or revoked, - unless we wanted to display denied or revoked enrollments in the UI, - we can let the TTL be, so that the enrollment will be deleted subsequently.*/ await _updateEnrollmentValueAndResetTTL( '$enrollmentKey$currentAtSign', enrollDataStoreValue); // when enrollment is approved store the apkamPublicKey of the enrollment @@ -252,13 +253,13 @@ class EnrollVerbHandler extends AbstractVerbHandler { var privKeyJson = {}; privKeyJson['value'] = enrollParams.encryptedDefaultEncryptedPrivateKey; await keyStore.put( - '$newEnrollmentId.$defaultEncryptionPrivateKey.$enrollManageNamespace$atSign', + '$newEnrollmentId.${AtConstants.defaultEncryptionPrivateKey}.$enrollManageNamespace$atSign', AtData()..data = jsonEncode(privKeyJson), skipCommit: true); var selfKeyJson = {}; selfKeyJson['value'] = enrollParams.encryptedDefaultSelfEncryptionKey; await keyStore.put( - '$newEnrollmentId.$defaultSelfEncryptionKey.$enrollManageNamespace$atSign', + '$newEnrollmentId.${AtConstants.defaultSelfEncryptionKey}.$enrollManageNamespace$atSign', AtData()..data = jsonEncode(selfKeyJson), skipCommit: true); } @@ -338,7 +339,7 @@ class EnrollVerbHandler extends AbstractVerbHandler { String key, EnrollParams enrollParams, String atSign) async { try { var notificationValue = {}; - notificationValue[apkamEncryptedSymmetricKey] = + notificationValue[AtConstants.apkamEncryptedSymmetricKey] = enrollParams.encryptedAPKAMSymmetricKey; logger.finer('notificationValue:$notificationValue'); final atNotification = (AtNotificationBuilder() @@ -355,10 +356,10 @@ class EnrollVerbHandler extends AbstractVerbHandler { logger.finer('notification generated: $notificationId'); } on Exception catch (e, trace) { logger.severe( - 'Exception while storing notification key $enrollmentId. Exception $e. Trace $trace'); + 'Exception while storing notification key ${AtConstants.enrollmentId}. Exception $e. Trace $trace'); } on Error catch (e, trace) { logger.severe( - 'Error while storing notification key $enrollmentId. Error $e. Trace $trace'); + 'Error while storing notification key ${AtConstants.enrollmentId}. Error $e. Trace $trace'); } } diff --git a/packages/at_secondary_server/lib/src/verb/handler/otp_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/otp_verb_handler.dart index ac8839cb7..b9b9ae7cd 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/otp_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/otp_verb_handler.dart @@ -1,24 +1,24 @@ import 'dart:collection'; import 'dart:math'; import 'package:at_commons/at_commons.dart'; +import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_server_spec/at_server_spec.dart'; +import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; import 'abstract_verb_handler.dart'; import 'package:at_server_spec/at_verb_spec.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; -import 'package:expire_cache/expire_cache.dart'; class OtpVerbHandler extends AbstractVerbHandler { static Otp otpVerb = Otp(); - static final expireDuration = Duration(seconds: 90); - static ExpireCache cache = - ExpireCache(expireDuration: expireDuration); + + @visibleForTesting + int otpExpiryInMills = Duration(minutes: 5).inMilliseconds; OtpVerbHandler(SecondaryKeyStore keyStore) : super(keyStore); @override - bool accept(String command) => - command == 'otp:get' || command.startsWith('otp:validate'); + bool accept(String command) => command == 'otp:get'; @override Verb getVerb() => otpVerb; @@ -29,6 +29,10 @@ class OtpVerbHandler extends AbstractVerbHandler { HashMap verbParams, InboundConnection atConnection) async { final operation = verbParams['operation']; + if (verbParams[AtConstants.ttl] != null && + verbParams[AtConstants.ttl]!.isNotEmpty) { + otpExpiryInMills = int.parse(verbParams[AtConstants.ttl]!); + } switch (operation) { case 'get': if (!atConnection.getMetaData().isAuthenticated) { @@ -40,16 +44,15 @@ class OtpVerbHandler extends AbstractVerbHandler { } // If OTP generated do not have digits, generate again. while (RegExp(r'\d').hasMatch(response.data!) == false); - await cache.set(response.data!, response.data!); - break; - case 'validate': - String? otp = verbParams['otp']; - if (otp != null && (await cache.get(otp)) == otp) { - response.data = 'valid'; - } else { - response.data = 'invalid'; - } + await keyStore.put( + 'private:${response.data}${AtSecondaryServerImpl.getInstance().currentAtSign}', + AtData() + ..data = + '${DateTime.now().toUtc().add(Duration(milliseconds: otpExpiryInMills)).millisecondsSinceEpoch}' + ..metaData = (AtMetaData()..ttl = otpExpiryInMills)); break; + default: + throw InvalidSyntaxException('$operation is not a valid operation'); } } diff --git a/packages/at_secondary_server/pubspec.yaml b/packages/at_secondary_server/pubspec.yaml index 8b96aef6b..982ef2778 100644 --- a/packages/at_secondary_server/pubspec.yaml +++ b/packages/at_secondary_server/pubspec.yaml @@ -19,19 +19,18 @@ dependencies: basic_utils: 5.6.1 ecdsa: 0.0.4 encrypt: 5.0.3 - at_commons: 3.0.56 + at_commons: 3.0.57 at_utils: 3.0.15 at_chops: 1.0.4 at_lookup: 3.0.40 at_server_spec: 3.0.15 at_persistence_spec: 2.0.14 at_persistence_secondary_server: 3.0.58 - expire_cache: ^2.0.1 intl: ^0.18.1 json_annotation: ^4.8.0 version: 3.0.2 meta: 1.10.0 - mutex: 3.0.1 + mutex: 3.1.0 yaml: 3.1.2 logging: 1.2.0 diff --git a/packages/at_secondary_server/test/enroll_verb_test.dart b/packages/at_secondary_server/test/enroll_verb_test.dart index 6e48d943d..236bf5cb7 100644 --- a/packages/at_secondary_server/test/enroll_verb_test.dart +++ b/packages/at_secondary_server/test/enroll_verb_test.dart @@ -41,7 +41,6 @@ void main() { OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); await otpVerbHandler.processVerb( response, otpVerbParams, inboundConnection); - print('OTP: ${response.data}'); // Enroll request 2 enrollmentRequest = 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"buzz":"r"},"otp":"${response.data}","apkamPublicKey":"dummy_apkam_public_key"}'; @@ -81,6 +80,31 @@ void main() { expect(enrollmentValue.namespaces.containsKey('__manage'), true); expect(enrollmentValue.namespaces.containsKey('*'), true); }); + + test('A test to verify OTP is deleted once it is used to submit an enrollment',() async { + Response response = Response(); + // OTP Verb + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session'; + HashMap otpVerbParams = + getVerbParam(VerbSyntax.otp, 'otp:get'); + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + await otpVerbHandler.processVerb( + response, otpVerbParams, inboundConnection); + String otp = response.data!; + + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"buzz":"r"},"otp":"$otp","apkamPublicKey":"dummy_apkam_public_key"}'; + HashMap enrollmentRequestVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.getMetaData().isAuthenticated = false; + EnrollVerbHandler enrollVerbHandler = EnrollVerbHandler(secondaryKeyStore); + await enrollVerbHandler.processVerb( + response, enrollmentRequestVerbParams, inboundConnection); + String enrollmentId = jsonDecode(response.data!)['enrollmentId']; + expect(enrollmentId, isNotNull); + expect(await enrollVerbHandler.isOTPValid(otp), false); + }); tearDown(() async => await verbTestsTearDown()); }); group('A group of tests to verify enroll list operation', () { @@ -316,7 +340,7 @@ void main() { test('A test to verify enrollment request without otp throws exception', () async { String enrollmentRequest = - 'enroll:request:{"appname":"wavi","devicename":"mydevice","namespaces":{"wavi":"r"},"apkampublickey":"dummy_apkam_public_key"}'; + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"wavi":"r"},"apkamPublicKey":"dummy_apkam_public_key"}'; HashMap verbParams = getVerbParam(VerbSyntax.enroll, enrollmentRequest); inboundConnection.getMetaData().isAuthenticated = false; diff --git a/packages/at_secondary_server/test/otp_verb_test.dart b/packages/at_secondary_server/test/otp_verb_test.dart index 1f7a0276c..a113cf8ce 100644 --- a/packages/at_secondary_server/test/otp_verb_test.dart +++ b/packages/at_secondary_server/test/otp_verb_test.dart @@ -3,7 +3,6 @@ import 'dart:collection'; import 'package:at_commons/at_commons.dart'; import 'package:at_secondary/src/utils/handler_util.dart'; import 'package:at_secondary/src/verb/handler/otp_verb_handler.dart'; -import 'package:expire_cache/expire_cache.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; @@ -13,19 +12,20 @@ void main() { setUp(() async { await verbTestsSetUp(); }); - test('A test to verify OTP generated is 6-character length', () { + + test('A test to verify OTP generated is 6-character length', () async { Response response = Response(); HashMap verbParams = getVerbParam(VerbSyntax.otp, 'otp:get'); inboundConnection.getMetaData().isAuthenticated = true; OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); - otpVerbHandler.processVerb(response, verbParams, inboundConnection); + await otpVerbHandler.processVerb(response, verbParams, inboundConnection); expect(response.data, isNotNull); expect(response.data!.length, 6); assert(RegExp('\\d').hasMatch(response.data!)); }); - test('A test to verify same OTP is not returned', () { + test('A test to verify same OTP is not returned', () async { Set otpSet = {}; for (int i = 1; i <= 1000; i++) { Response response = Response(); @@ -33,7 +33,8 @@ void main() { getVerbParam(VerbSyntax.otp, 'otp:get'); inboundConnection.getMetaData().isAuthenticated = true; OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); - otpVerbHandler.processVerb(response, verbParams, inboundConnection); + await otpVerbHandler.processVerb( + response, verbParams, inboundConnection); expect(response.data, isNotNull); expect(response.data!.length, 6); assert(RegExp('\\d').hasMatch(response.data!)); @@ -42,6 +43,31 @@ void main() { } expect(otpSet.length, 1000); }); + + test('A test to verify otp:get with TTL set is active before TTL is met', + () async { + Response response = Response(); + inboundConnection.getMetaData().isAuthenticated = true; + HashMap verbParams = + getVerbParam(VerbSyntax.otp, 'otp:get:ttl:1000'); + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + await otpVerbHandler.processVerb(response, verbParams, inboundConnection); + String? otp = response.data; + expect(await otpVerbHandler.isOTPValid(otp), true); + }); + + test('A test to verify otp:get with TTL set expires after the TTL is met', + () async { + Response response = Response(); + inboundConnection.getMetaData().isAuthenticated = true; + HashMap verbParams = + getVerbParam(VerbSyntax.otp, 'otp:get:ttl:1'); + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + await otpVerbHandler.processVerb(response, verbParams, inboundConnection); + String? otp = response.data; + await Future.delayed(Duration(seconds: 1)); + expect(await otpVerbHandler.isOTPValid(otp), false); + }); tearDown(() async => await verbTestsTearDown()); }); @@ -66,11 +92,11 @@ void main() { tearDown(() async => await verbTestsTearDown()); }); - group('A group of tests related to otp:validate', () { + group('A group of tests related to OTP validity', () { setUp(() async { await verbTestsSetUp(); }); - test('A test to verify otp:validate returns valid when OTP is active', + test('A test to verify isOTPValid method returns valid when OTP is active', () async { Response response = Response(); HashMap verbParams = @@ -78,10 +104,7 @@ void main() { inboundConnection.getMetaData().isAuthenticated = true; OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); await otpVerbHandler.processVerb(response, verbParams, inboundConnection); - verbParams = - getVerbParam(VerbSyntax.otp, 'otp:validate:${response.data}'); - await otpVerbHandler.processVerb(response, verbParams, inboundConnection); - expect(response.data, 'valid'); + expect(await otpVerbHandler.isOTPValid(response.data), true); }); test('A test to verify otp:validate returns invalid when OTP is expired', @@ -90,16 +113,20 @@ void main() { HashMap verbParams = getVerbParam(VerbSyntax.otp, 'otp:get'); inboundConnection.getMetaData().isAuthenticated = true; - OtpVerbHandler.cache = - ExpireCache(expireDuration: Duration(microseconds: 1)); OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); - print(OtpVerbHandler.cache.expireDuration.inMicroseconds); - await Future.delayed(Duration(microseconds: 2)); + otpVerbHandler.otpExpiryInMills = 1; await otpVerbHandler.processVerb(response, verbParams, inboundConnection); - verbParams = - getVerbParam(VerbSyntax.otp, 'otp:validate:${response.data}'); - await otpVerbHandler.processVerb(response, verbParams, inboundConnection); - expect(response.data, 'invalid'); + String? otp = response.data; + await Future.delayed(Duration(milliseconds: 2)); + expect(await otpVerbHandler.isOTPValid(otp), false); + }); + + test( + 'A test to verify otp:validate return invalid when otp does not exist in keystore', + () async { + String otp = 'ABC123'; + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + expect(await otpVerbHandler.isOTPValid(otp), false); }); tearDown(() async => await verbTestsTearDown()); }); diff --git a/tests/at_functional_test/test/enroll_verb_test.dart b/tests/at_functional_test/test/enroll_verb_test.dart index bf9d6f4b9..537929bb8 100644 --- a/tests/at_functional_test/test/enroll_verb_test.dart +++ b/tests/at_functional_test/test/enroll_verb_test.dart @@ -767,9 +767,6 @@ void main() { socketConnection1!, 'config:set:timeFrameInMills=100\n'); configResponse = await read(); expect(configResponse.trim(), 'data:ok'); - await socket_writer(socketConnection1!, 'otp:get'); - otp = await read(); - otp = otp.replaceAll('data:', '').trim(); }); test( @@ -778,6 +775,9 @@ void main() { SecureSocket unAuthenticatedConnection = await secure_socket_connection(firstAtsignServer, firstAtsignPort); socket_listener(unAuthenticatedConnection); + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); var enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(unAuthenticatedConnection, enrollRequest); @@ -785,6 +785,10 @@ void main() { jsonDecode((await read()).replaceAll('data:', '')); expect(enrollmentResponse['status'], 'pending'); expect(enrollmentResponse['enrollmentId'], isNotNull); + + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(unAuthenticatedConnection, enrollRequest); @@ -801,6 +805,10 @@ void main() { SecureSocket unAuthenticatedConnection = await secure_socket_connection(firstAtsignServer, firstAtsignPort); socket_listener(unAuthenticatedConnection); + + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); var enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(unAuthenticatedConnection, enrollRequest); @@ -808,6 +816,10 @@ void main() { jsonDecode((await read()).replaceAll('data:', '')); expect(enrollmentResponse['status'], 'pending'); expect(enrollmentResponse['enrollmentId'], isNotNull); + + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(unAuthenticatedConnection, enrollRequest); @@ -828,6 +840,10 @@ void main() { SecureSocket unAuthenticatedConnection = await secure_socket_connection(firstAtsignServer, firstAtsignPort); socket_listener(unAuthenticatedConnection); + + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); var enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(unAuthenticatedConnection, enrollRequest); @@ -835,6 +851,10 @@ void main() { jsonDecode((await read()).replaceAll('data:', '')); expect(enrollmentResponse['status'], 'pending'); expect(enrollmentResponse['enrollmentId'], isNotNull); + + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(unAuthenticatedConnection, enrollRequest); @@ -847,6 +867,10 @@ void main() { SecureSocket secondUnAuthenticatedConnection2 = await secure_socket_connection(firstAtsignServer, firstAtsignPort); socket_listener(secondUnAuthenticatedConnection2); + + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); enrollRequest = 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; await socket_writer(secondUnAuthenticatedConnection2, enrollRequest); diff --git a/tools/requirements.txt b/tools/requirements.txt index ed419bd0b..fb287e001 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -108,17 +108,22 @@ ruamel-yaml-clib==0.2.8 ; platform_python_implementation == "CPython" and python --hash=sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15 \ --hash=sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b \ --hash=sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675 \ + --hash=sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1 \ --hash=sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7 \ --hash=sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312 \ + --hash=sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f \ --hash=sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91 \ + --hash=sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa \ --hash=sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b \ --hash=sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3 \ --hash=sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5 \ --hash=sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe \ + --hash=sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3 \ --hash=sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed \ --hash=sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337 \ --hash=sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248 \ --hash=sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d \ + --hash=sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279 \ --hash=sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf \ --hash=sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512 \ --hash=sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069 \ @@ -127,6 +132,7 @@ ruamel-yaml-clib==0.2.8 ; platform_python_implementation == "CPython" and python --hash=sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d \ --hash=sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31 \ --hash=sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92 \ + --hash=sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd \ --hash=sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5 \ --hash=sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1 \ --hash=sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2 \ @@ -135,6 +141,6 @@ ruamel-yaml-clib==0.2.8 ; platform_python_implementation == "CPython" and python ruamel-yaml==0.17.35 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:801046a9caacb1b43acc118969b49b96b65e8847f29029563b29ac61d02db61b \ --hash=sha256:b105e3e6fc15b41fdb201ba1b95162ae566a4ef792b9f884c46b4ccc5513a87a -urllib3==2.0.6 ; python_version >= "3.8" and python_version < "4.0" \ - --hash=sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2 \ - --hash=sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564 +urllib3==2.0.7 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \ + --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e