diff --git a/aws-analytics-pinpoint/build.gradle.kts b/aws-analytics-pinpoint/build.gradle.kts index b563cc6805..c3c99f6fc4 100644 --- a/aws-analytics-pinpoint/build.gradle.kts +++ b/aws-analytics-pinpoint/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-auth-cognito/build.gradle.kts b/aws-auth-cognito/build.gradle.kts index 7a002160da..0a0a7e1496 100644 --- a/aws-auth-cognito/build.gradle.kts +++ b/aws-auth-cognito/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt index 4d00bf04bd..d845168276 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt @@ -18,9 +18,9 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.runtime.http.operation.customUserAgentMetadata import aws.sdk.kotlin.services.cognitoidentity.CognitoIdentityClient import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.endpoints.CognitoIdentityProviderEndpointProvider import aws.smithy.kotlin.runtime.client.RequestInterceptorContext import aws.smithy.kotlin.runtime.client.endpoints.Endpoint -import aws.smithy.kotlin.runtime.client.endpoints.EndpointProvider import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor import com.amplifyframework.statemachine.codegen.data.AuthConfiguration @@ -36,7 +36,7 @@ interface AWSCognitoAuthService { CognitoIdentityProviderClient { this.region = it.region this.endpointProvider = it.endpoint?.let { endpoint -> - EndpointProvider { Endpoint(endpoint) } + CognitoIdentityProviderEndpointProvider { Endpoint(endpoint) } } this.interceptors += object : HttpInterceptor { override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext): Any { diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt index 3fb992ce1f..032082aab6 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt @@ -512,6 +512,7 @@ class RealAWSCognitoAuthPluginTest { authService.cognitoIdentityProviderClient?.getUser(any()) } returns GetUserResponse.invoke { this.userAttributes = userAttributes + username = "" } every { @@ -1806,6 +1807,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } plugin.fetchMFAPreference(onSuccess, onError) @@ -1842,6 +1845,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, onError) @@ -1887,6 +1892,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } @@ -1937,6 +1944,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } @@ -2040,6 +2049,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2090,6 +2101,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2140,6 +2153,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2190,6 +2205,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2240,6 +2257,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2290,6 +2309,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2340,6 +2361,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2390,6 +2413,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } @@ -2440,6 +2465,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } @@ -2490,6 +2517,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } @@ -2540,6 +2569,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } @@ -2590,6 +2621,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt index 9a6dfeabae..954e800442 100644 --- a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt +++ b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt @@ -77,6 +77,7 @@ class CognitoMockFactory( this.userConfirmed = if (responseObject.containsKey("userConfirmed")) { (responseObject["userConfirmed"] as? JsonPrimitive)?.boolean ?: false } else false + this.userSub = "" } } } @@ -139,6 +140,7 @@ class CognitoMockFactory( value = "000-000-0000" } ) + username = "" } } } diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json index 156fa4a187..902a401f73 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json @@ -57,7 +57,8 @@ "signUpStep": "DONE", "additionalInfo": { } - } + }, + "userId": "" } } ] diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json index 767b69b838..8ea70d469e 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json @@ -61,7 +61,8 @@ "deliveryMedium": "EMAIL", "attributeName": "attributeName" } - } + }, + "userId": "" } } ] diff --git a/aws-auth-plugins-core/build.gradle.kts b/aws-auth-plugins-core/build.gradle.kts index 94f9546f5c..5c5a568869 100644 --- a/aws-auth-plugins-core/build.gradle.kts +++ b/aws-auth-plugins-core/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-logging-cloudwatch/build.gradle.kts b/aws-logging-cloudwatch/build.gradle.kts index d5b57024f4..d2295f9169 100644 --- a/aws-logging-cloudwatch/build.gradle.kts +++ b/aws-logging-cloudwatch/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-pinpoint-core/build.gradle.kts b/aws-pinpoint-core/build.gradle.kts index a4030852f1..a35762d90e 100644 --- a/aws-pinpoint-core/build.gradle.kts +++ b/aws-pinpoint-core/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt b/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt index 519d223e36..b289c8ba4c 100644 --- a/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt +++ b/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt @@ -40,6 +40,7 @@ import com.amplifyframework.pinpoint.core.database.PinpointDatabase import com.amplifyframework.pinpoint.core.endpointProfile.EndpointProfile import com.amplifyframework.pinpoint.core.models.PinpointEvent import com.amplifyframework.pinpoint.core.util.millisToIsoDate +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -59,7 +60,7 @@ class EventRecorder( AWS_PINPOINT_ANALYTICS_LOG_NAMESPACE.format(EventRecorder::class.java.simpleName) ) ) { - private var isSyncInProgress = false + private var isSyncInProgress = AtomicBoolean(false) private val defaultMaxSubmissionAllowed = 3 private val defaultMaxSubmissionSize = 1024 * 100 private val serviceDefinedMaxEventsPerBatch: Int = 100 @@ -79,12 +80,10 @@ class EventRecorder( } } - @Synchronized internal suspend fun submitEvents(): List { return withContext(coroutineDispatcher) { val result = runCatching { - if (!isSyncInProgress) { - isSyncInProgress = true + if (isSyncInProgress.compareAndSet(false, true)) { processEvents() } else { logger.info("Sync is already in progress, skipping") @@ -93,11 +92,11 @@ class EventRecorder( } when { result.isSuccess -> { - isSyncInProgress = false + isSyncInProgress.set(false) result.getOrNull() ?: emptyList() } else -> { - isSyncInProgress = false + isSyncInProgress.set(false) logger.error("Failed to submit events ${result.exceptionOrNull()}") emptyList() } diff --git a/aws-predictions/build.gradle.kts b/aws-predictions/build.gradle.kts index 7b64243839..9db0c3a5e9 100644 --- a/aws-predictions/build.gradle.kts +++ b/aws-predictions/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt index a93452907d..2da96d3027 100755 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt @@ -56,6 +56,11 @@ internal class AWSV4Signer { timeFormatter.isLenient = false } + // used in incorrect time flow where we send an invalid response first to get time offset + fun resetPriorSignature() { + priorSignature = "" + } + fun getSignedUri( uri: URI, credentials: Credentials, diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 2009ca60a3..ad4c8c1e61 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -46,7 +46,9 @@ import com.amplifyframework.util.UserAgent import java.net.URI import java.net.URLDecoder import java.nio.ByteBuffer +import java.text.SimpleDateFormat import java.util.Date +import java.util.Locale import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -76,6 +78,24 @@ internal class LivenessWebSocket( private val signer = AWSV4Signer() private var credentials: Credentials? = null + internal var offset = 0L + internal enum class ReconnectState { + INITIAL, + RECONNECTING, + RECONNECTING_AGAIN; + + companion object { + fun next(state: ReconnectState): ReconnectState { + return when (state) { + INITIAL -> RECONNECTING + RECONNECTING -> RECONNECTING_AGAIN + RECONNECTING_AGAIN -> RECONNECTING_AGAIN + } + } + } + } + internal var reconnectState = ReconnectState.INITIAL + @VisibleForTesting internal var webSocket: WebSocket? = null internal val challengeId = UUID.randomUUID().toString() @@ -91,8 +111,24 @@ internal class LivenessWebSocket( internal var webSocketListener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { LOG.debug("WebSocket onOpen") - super.onOpen(webSocket, response) - this@LivenessWebSocket.webSocket = webSocket + + // device time may be set incorrectly; read the header to skew time and retry + val sdf = SimpleDateFormat(datePattern, Locale.US) + val date = response.header("Date")?.let { sdf.parse(it) } + val tempOffset = if (date != null) { + date.time - adjustedDate() + } else 0 + + reconnectState = ReconnectState.next(reconnectState) + // if offset is > 5 minutes, server will reject the request + if (kotlin.math.abs(tempOffset) < FIVE_MINUTES) { + super.onOpen(webSocket, response) + this@LivenessWebSocket.webSocket = webSocket + } else { + // server will close this websocket, don't report that failure back + offset = tempOffset + start() + } } override fun onMessage(webSocket: WebSocket, text: String) { @@ -110,7 +146,10 @@ internal class LivenessWebSocket( livenessResponse.serverSessionInformationEvent.sessionInformation ) } else if (livenessResponse.disconnectionEvent != null) { - this@LivenessWebSocket.webSocket?.close(1000, "Liveness flow completed.") + this@LivenessWebSocket.webSocket?.close( + NORMAL_SOCKET_CLOSURE_STATUS_CODE, + "Liveness flow completed." + ) } else { handleWebSocketError(livenessResponse) } @@ -130,7 +169,9 @@ internal class LivenessWebSocket( override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { LOG.debug("WebSocket onClosed") super.onClosed(webSocket, code, reason) - if (code != 1000 && !clientStoppedSession) { + if (reconnectState == ReconnectState.RECONNECTING) { + // do nothing; we expected the server to close the connection + } else if (code != NORMAL_SOCKET_CLOSURE_STATUS_CODE && !clientStoppedSession) { val faceLivenessException = webSocketError ?: PredictionsException( "An error occurred during the face liveness check.", reason @@ -156,6 +197,14 @@ internal class LivenessWebSocket( } fun start() { + if (reconnectState == ReconnectState.RECONNECTING_AGAIN) { + onErrorReceived.accept( + PredictionsException( + "Invalid device time", + "Too many attempts were made to correct device time" + ) + ) + } val userAgent = getUserAgent() val okHttpClient = OkHttpClient.Builder() @@ -173,7 +222,10 @@ internal class LivenessWebSocket( try { val credentials = credentialsProvider.resolve(emptyAttributes()) this@LivenessWebSocket.credentials = credentials - val signedUri = signer.getSignedUri(URI.create(endpoint), credentials, region, userAgent) + signer.resetPriorSignature() + val signedUri = signer.getSignedUri( + URI.create(endpoint), credentials, region, userAgent, adjustedDate() + ) if (signedUri != null) { val signedEndpoint = URLDecoder.decode(signedUri.toString(), "UTF-8") val signedEndpointNoSpaces = signedEndpoint.replace(" ", signer.encodedSpace) @@ -274,14 +326,14 @@ internal class LivenessWebSocket( videoStartTime: Long ) { // Send initial ClientSessionInformationEvent - videoStartTimestamp = videoStartTime + videoStartTimestamp = adjustedDate(videoStartTime) initialDetectedFace = BoundingBox( left = initialFaceRect.left / sessionInformation.videoWidth, top = initialFaceRect.top / sessionInformation.videoHeight, height = initialFaceRect.height() / sessionInformation.videoHeight, width = initialFaceRect.width() / sessionInformation.videoWidth ) - faceDetectedStart = videoStartTime + faceDetectedStart = adjustedDate(videoStartTime) val clientInfoEvent = ClientSessionInformationEvent( challenge = ClientChallenge( @@ -309,8 +361,8 @@ internal class LivenessWebSocket( initialFaceDetectedTimestamp = faceDetectedStart ), targetFace = TargetFace( - faceDetectedInTargetPositionStartTimestamp = faceMatchedStart, - faceDetectedInTargetPositionEndTimestamp = faceMatchedEnd, + faceDetectedInTargetPositionStartTimestamp = adjustedDate(faceMatchedStart), + faceDetectedInTargetPositionEndTimestamp = adjustedDate(faceMatchedEnd), boundingBox = BoundingBox( left = targetFaceRect.left / sessionInformation.videoWidth, top = targetFaceRect.top / sessionInformation.videoHeight, @@ -338,7 +390,7 @@ internal class LivenessWebSocket( currentColor = currentColor, previousColor = previousColor, sequenceNumber = sequenceNumber, - currentColorStartTimestamp = colorStartTime + currentColorStartTimestamp = adjustedDate(colorStartTime) ) ) ) @@ -358,7 +410,7 @@ internal class LivenessWebSocket( ":content-type" to "application/json" ) ) - val eventDate = Date() + val eventDate = Date(adjustedDate()) val signedPayload = signer.getSignedFrame( region, encodedPayload.array(), @@ -381,12 +433,12 @@ internal class LivenessWebSocket( fun sendVideoEvent(videoBytes: ByteArray, videoEventTime: Long) { if (videoBytes.isNotEmpty()) { - videoEndTimestamp = videoEventTime + videoEndTimestamp = adjustedDate(videoEventTime) } credentials?.let { val videoBuffer = ByteBuffer.wrap(videoBytes) val videoEvent = VideoEvent( - timestampMillis = videoEventTime, + timestampMillis = adjustedDate(videoEventTime), videoChunk = videoBuffer ) val videoJsonString = Json.encodeToString(videoEvent) @@ -399,7 +451,7 @@ internal class LivenessWebSocket( ":content-type" to "application/json" ) ) - val videoEventDate = Date() + val videoEventDate = Date(adjustedDate()) val signedVideoPayload = signer.getSignedFrame( region, encodedVideoPayload.array(), @@ -420,11 +472,18 @@ internal class LivenessWebSocket( } fun destroy() { - // Close gracefully; 1000 means "normal closure" - webSocket?.close(1000, null) + // Close gracefully + webSocket?.close(NORMAL_SOCKET_CLOSURE_STATUS_CODE, null) + } + + fun adjustedDate(date: Long = Date().time): Long { + return date + offset } companion object { + private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000 + private val FIVE_MINUTES = 1000 * 60 * 5 + @VisibleForTesting val datePattern = "EEE, d MMM yyyy HH:mm:ss z" private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions") } } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt new file mode 100644 index 0000000000..42cfa685ef --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.models.liveness + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Constructs a new InvalidSignatureException with the specified error message. + * + * @param message Describes the error encountered. + */ +@Serializable +internal data class InvalidSignatureException( + @SerialName("Message") override val message: String +) : Exception(message) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt index f6348ac9ad..0d9c6e60d8 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt @@ -29,5 +29,6 @@ internal data class LivenessResponseStream( ServiceQuotaExceededException? = null, @SerialName("ServiceUnavailableException") val serviceUnavailableException: ServiceUnavailableException? = null, @SerialName("SessionNotFoundException") val sessionNotFoundException: SessionNotFoundException? = null, - @SerialName("AccessDeniedException") val accessDeniedException: AccessDeniedException? = null + @SerialName("AccessDeniedException") val accessDeniedException: AccessDeniedException? = null, + @SerialName("InvalidSignatureException") val invalidSignatureException: InvalidSignatureException? = null ) diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index 7ab4b99cf9..42756f0932 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -35,11 +35,18 @@ import com.amplifyframework.predictions.aws.models.liveness.ServerSessionInforma import com.amplifyframework.predictions.aws.models.liveness.SessionInformation import com.amplifyframework.predictions.aws.models.liveness.ValidationException import com.amplifyframework.predictions.models.FaceLivenessSessionInformation +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkConstructor import io.mockk.verify import java.net.URL +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.math.abs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.resetMain @@ -48,6 +55,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import mockwebserver3.MockResponse import mockwebserver3.MockWebServer +import okhttp3.Headers import okhttp3.Protocol import okhttp3.Request import okhttp3.Response @@ -57,7 +65,6 @@ import okio.ByteString import okio.ByteString.Companion.toByteString import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore @@ -189,14 +196,12 @@ internal class LivenessWebSocketTest { assertTrue(livenessWebSocket.webSocket != null) val originalRequest = livenessWebSocket.webSocket!!.request() - assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) assertTrue( originalRequest.url.queryParameter("X-Amz-Credential")!!.endsWith("//rekognition/aws4_request") ) assertEquals("299", originalRequest.url.queryParameter("X-Amz-Expires")) assertEquals("host", originalRequest.url.queryParameter("X-Amz-SignedHeaders")) assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) - assertNotNull("x-amz-user-agent") } @Test @@ -317,6 +322,54 @@ internal class LivenessWebSocketTest { assertEquals(livenessWebSocket.getUserAgent(), "$baseline $additional") } + @Test + fun `web socket detects clock skew from server response`() { + val livenessWebSocket = createLivenessWebSocket() + mockkConstructor(WebSocket::class) + val socket: WebSocket = mockk() + livenessWebSocket.webSocket = socket + val sdf = SimpleDateFormat(LivenessWebSocket.datePattern, Locale.US) + + // server responds saying time is actually 1 hour in the future + val oneHour = 1000 * 3600 + val futureDate = sdf.format(Date(Date().time + oneHour)) + val response: Response = mockk() + every { response.headers }.returns(Headers.headersOf("Date", futureDate)) + every { response.header("Date") }.returns(futureDate) + livenessWebSocket.webSocketListener.onOpen(socket, response) + + // now we should restart the websocket with an adjusted time + val openLatch = CountDownLatch(1) + val latchingListener = LatchingWebSocketResponseListener( + livenessWebSocket.webSocketListener, + openLatch = openLatch + ) + livenessWebSocket.webSocketListener = latchingListener + + server.enqueue(MockResponse().withWebSocketUpgrade(ServerWebSocketListener())) + server.start() + + openLatch.await(3, TimeUnit.SECONDS) + + assertTrue(livenessWebSocket.webSocket != null) + val originalRequest = livenessWebSocket.webSocket!!.request() + + // make sure that followup request sends offset date + val sdfGMT = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US) + sdfGMT.timeZone = TimeZone.getTimeZone("GMT") + val sentDate = originalRequest.url.queryParameter("X-Amz-Date") ?.let { sdfGMT.parse(it) } + val diff = abs(Date().time - sentDate?.time!!) + assert(oneHour - 10000 < diff && diff < oneHour + 10000) + + // also make sure that followup request is valid + assertTrue( + originalRequest.url.queryParameter("X-Amz-Credential")!!.endsWith("//rekognition/aws4_request") + ) + assertEquals("299", originalRequest.url.queryParameter("X-Amz-Expires")) + assertEquals("host", originalRequest.url.queryParameter("X-Amz-SignedHeaders")) + assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) + } + @Test @Ignore("Need to work on parsing the onMessage byteString from ServerWebSocketListener") fun `sendInitialFaceDetectedEvent test`() { diff --git a/build.gradle.kts b/build.gradle.kts index 0155bdb33b..a02cc75d3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:7.3.1") - classpath(kotlin("gradle-plugin", version = "1.7.10")) + classpath(kotlin("gradle-plugin", version = "1.9.10")) classpath("com.google.gms:google-services:4.3.15") classpath("org.jlleitschuh.gradle:ktlint-gradle:11.0.0") classpath("org.gradle:test-retry-gradle-plugin:1.4.1") diff --git a/canaries/example/build.gradle b/canaries/example/build.gradle index 088af1ae8b..feb90d5b24 100644 --- a/canaries/example/build.gradle +++ b/canaries/example/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.8.0" + ext.kotlin_version = "1.9.10" repositories { google() jcenter() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2059cd784a..46a1474009 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,10 +15,10 @@ androidx-test-junit = "1.1.2" androidx-test-orchestrator = "1.3.0" androidx-test-runner = "1.3.0" androidx-workmanager = "2.7.1" -aws-kotlin = "0.29.1-beta" # ensure proper aws-smithy version also set +aws-kotlin = "0.33.1-beta" # ensure proper aws-smithy version also set aws-sdk = "2.62.2" -aws-smithy = "0.25.0" # ensure proper aws-kotlin version also set -coroutines = "1.6.3" +aws-smithy = "0.28.1" # ensure proper aws-kotlin version also set +coroutines = "1.7.3" desugar = "1.2.0" espresso = "3.3.0" fcm = "23.1.0" @@ -27,8 +27,8 @@ json = "20210307" jsonassert = "1.5.0" junit = "4.13.2" kotest = "5.6.2" -kotlin = "1.7.10" -kotlin-serialization = "1.3.3" +kotlin = "1.9.10" +kotlin-serialization = "1.6.0" maplibre = "9.6.0" maplibre-annotations = "1.0.0" material = "1.8.0" diff --git a/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt b/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt index 1a73511c71..6877361f87 100644 --- a/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt +++ b/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt @@ -19,8 +19,8 @@ import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner import aws.smithy.kotlin.runtime.http.Headers as AwsHeaders +import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.HttpMethod -import aws.smithy.kotlin.runtime.http.content.ByteArrayContent import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.Host import aws.smithy.kotlin.runtime.net.QueryParameters @@ -125,7 +125,7 @@ internal class AWSRequestSignerInterceptor( ) val bodyBytes: ByteArray = getBytes(request.body) - val body2 = ByteArrayContent(bodyBytes) + val body2 = HttpBody.fromBytes(bodyBytes) val method = HttpMethod.parse(request.method) val awsRequest = HttpRequest(method, httpUrl, headers, body2) diff --git a/testutils/build.gradle.kts b/testutils/build.gradle.kts index 63a600e402..0bfecd69d3 100644 --- a/testutils/build.gradle.kts +++ b/testutils/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") }