diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt index 136568bf4..825b07db5 100644 --- a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt +++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt @@ -33,6 +33,7 @@ import me.proton.core.crypto.common.context.CryptoContext import me.proton.core.crypto.common.keystore.KeyStoreCrypto import me.proton.core.crypto.common.pgp.PGPCrypto import me.proton.core.crypto.common.srp.SrpCrypto +import me.proton.core.crypto.common.srp.SrpChallenge import me.proton.core.crypto.dagger.CoreCryptoModule import me.proton.core.network.data.di.AlternativeApiPins import me.proton.core.network.data.di.CertificatePins @@ -44,6 +45,7 @@ import me.proton.core.test.android.mocks.FakeApiClient import me.proton.core.test.android.mocks.FakeKeyStoreCrypto import me.proton.core.test.android.mocks.FakePGPCrypto import me.proton.core.test.android.mocks.FakeSrpCrypto +import me.proton.core.test.android.mocks.FakeSrpChallenge import me.proton.core.test.android.mocks.FakeBillingClientFactory import javax.inject.Singleton @@ -117,4 +119,8 @@ object TestComponent { @Singleton fun provideWorkManager(): WorkManager = mockk(relaxed = true) // endregion + + @Provides + @Singleton + fun provideSrpChallenge(): SrpChallenge = FakeSrpChallenge() } diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeSrpChallenge.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeSrpChallenge.kt new file mode 100644 index 000000000..fc1495fa3 --- /dev/null +++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeSrpChallenge.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.test.android.mocks + +import me.proton.core.crypto.common.srp.SrpChallenge + +@Suppress("MaxLineLength") +class FakeSrpChallenge : SrpChallenge { + override suspend fun argon2PreimageChallenge(challenge: String): String { + return "ewAAAAAAAABXe+n/4g0Hfz40eEw7h5d3XeiKdWilfCJvz0izj7p0YA==" + + } + + override suspend fun ecdlpChallenge(challenge: String): String { + return "ewAAAAAAAABXe+n/4g0Hfz40eEw7h5d3XeiKdWilfCJvz0izj7p0YA==" + } +} diff --git a/crypto/android/api/crypto-android.api b/crypto/android/api/crypto-android.api index fe579ce68..4fcf6676b 100644 --- a/crypto/android/api/crypto-android.api +++ b/crypto/android/api/crypto-android.api @@ -120,6 +120,12 @@ public final class me/proton/core/crypto/android/pgp/VerificationStatusKt { public static final fun toVerificationStatus (Lcom/proton/gopenpgp/crypto/SignatureVerificationError;)Lme/proton/core/crypto/common/pgp/VerificationStatus; } +public final class me/proton/core/crypto/android/srp/GOpenPGPSrpChallenge : me/proton/core/crypto/common/srp/SrpChallenge { + public fun ()V + public fun argon2PreimageChallenge (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun ecdlpChallenge (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class me/proton/core/crypto/android/srp/GOpenPGPSrpCrypto : me/proton/core/crypto/common/srp/SrpCrypto { public static final field Companion Lme/proton/core/crypto/android/srp/GOpenPGPSrpCrypto$Companion; public static final field SRP_BIT_LENGTH I diff --git a/crypto/android/src/androidTest/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallengeTest.kt b/crypto/android/src/androidTest/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallengeTest.kt index e23890536..4bf74ab1c 100644 --- a/crypto/android/src/androidTest/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallengeTest.kt +++ b/crypto/android/src/androidTest/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallengeTest.kt @@ -19,6 +19,7 @@ package me.proton.core.crypto.android.srp import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.test.assertFails @@ -27,7 +28,7 @@ internal class GOpenPGPSrpChallengeTest { private val challenge = GOpenPGPSrpChallenge() @Test - fun testArgon2PreimageChallenge() { + fun testArgon2PreimageChallenge() = runTest { val b64Challenge = "qbYJSn07JQGfol0u8MJTZ16fDRyFo2AR6phcgqlZCr44RBpz/odJc17EROMfMOpz2dE8oHW2JHeqoRax2ha4bpGusDBkEySSWJU+cmuWePzUC58fTY+VJMLBMDLhdqV9QKvozeqKcoPzqDoHZZYmyWQf4DIAKfgaha/WwzMikQMBAAAAIAAAAOEQAAABAAAA" val actual = challenge.argon2PreimageChallenge(b64Challenge) var expected = "ewAAAAAAAABXe+n/4g0Hfz40eEw7h5d3XeiKdWilfCJvz0izj7p0YA==" @@ -35,7 +36,7 @@ internal class GOpenPGPSrpChallengeTest { } @Test - fun testEcdlpChallenge() { + fun testEcdlpChallenge() = runTest { val challenge = "kavkPtdQF/bQMvMlCjfgMdRdMsIsA8DP0X0/p44n+6jcchSeEewrjqcwy0FYF0jkWO1Wz1pdSe3meRNtpf+g2DQluiIbobuq4mM7J45fabUlKRtbEhSogoc9H3S74Wlj" val expected = "ngAAAAAAAAAczZrEZLqS9+TGdB7vNex1HzvPpFJD7Qd4+yPEgGduDw==" val actual = GOpenPGPSrpChallenge().ecdlpChallenge(challenge) @@ -43,13 +44,13 @@ internal class GOpenPGPSrpChallengeTest { } @Test - fun testArgon2PreimageChallengeWithInvalidInput() { + fun testArgon2PreimageChallengeWithInvalidInput() = runTest { val invalidB64Challenge = "11123123144444" assertFails { challenge.argon2PreimageChallenge(invalidB64Challenge) } } @Test - fun testEcdlpChallengeWithEmptyInput() { + fun testEcdlpChallengeWithEmptyInput() = runTest { assertFails { challenge.ecdlpChallenge("") } } } \ No newline at end of file diff --git a/crypto/android/src/main/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallenge.kt b/crypto/android/src/main/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallenge.kt index 2d4690179..c300fb810 100644 --- a/crypto/android/src/main/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallenge.kt +++ b/crypto/android/src/main/kotlin/me/proton/core/crypto/android/srp/GOpenPGPSrpChallenge.kt @@ -19,18 +19,19 @@ package me.proton.core.crypto.android.srp import me.proton.core.crypto.common.srp.SrpChallenge +import javax.inject.Inject /** * Implementation of [SrpChallenge] using the gopenpgp srp library. */ -class GOpenPGPSrpChallenge : SrpChallenge { +class GOpenPGPSrpChallenge @Inject constructor(): SrpChallenge { - override fun argon2PreimageChallenge(challenge: String): String { + override suspend fun argon2PreimageChallenge(challenge: String): String { return com.proton.gopenpgp.srp.Srp.argon2PreimageChallenge(challenge, -1) } - override fun ecdlpChallenge(challenge: String): String { + override suspend fun ecdlpChallenge(challenge: String): String { return com.proton.gopenpgp.srp.Srp.ecdlpChallenge(challenge, -1) } } \ No newline at end of file diff --git a/crypto/common/api/crypto-common.api b/crypto/common/api/crypto-common.api index bdfbb880c..028d1e58d 100644 --- a/crypto/common/api/crypto-common.api +++ b/crypto/common/api/crypto-common.api @@ -472,6 +472,11 @@ public final class me/proton/core/crypto/common/srp/Auth { public fun toString ()Ljava/lang/String; } +public abstract interface class me/proton/core/crypto/common/srp/SrpChallenge { + public abstract fun argon2PreimageChallenge (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun ecdlpChallenge (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract interface class me/proton/core/crypto/common/srp/SrpCrypto { public abstract fun calculatePasswordVerifier (Ljava/lang/String;[BLjava/lang/String;Ljava/lang/String;)Lme/proton/core/crypto/common/srp/Auth; public abstract fun generateSrpProofs (Ljava/lang/String;[BJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/crypto/common/srp/SrpProofs; diff --git a/crypto/common/src/main/kotlin/me/proton/core/crypto/common/srp/SrpChallenge.kt b/crypto/common/src/main/kotlin/me/proton/core/crypto/common/srp/SrpChallenge.kt index c35d7817b..75ab94b77 100644 --- a/crypto/common/src/main/kotlin/me/proton/core/crypto/common/srp/SrpChallenge.kt +++ b/crypto/common/src/main/kotlin/me/proton/core/crypto/common/srp/SrpChallenge.kt @@ -29,8 +29,8 @@ typealias Based64Challenge = String; interface SrpChallenge { //argon2 preimage challenge - fun argon2PreimageChallenge(challenge: Based64Challenge): String; + suspend fun argon2PreimageChallenge(challenge: Based64Challenge): String; // ecdlp challenge - fun ecdlpChallenge(challenge: Based64Challenge): String; + suspend fun ecdlpChallenge(challenge: Based64Challenge): String; } \ No newline at end of file diff --git a/crypto/dagger/api/crypto-dagger.api b/crypto/dagger/api/crypto-dagger.api index 7f69c9c20..5ce164ca2 100644 --- a/crypto/dagger/api/crypto-dagger.api +++ b/crypto/dagger/api/crypto-dagger.api @@ -13,6 +13,7 @@ public final class me/proton/core/crypto/dagger/CoreCryptoModule { public static final field INSTANCE Lme/proton/core/crypto/dagger/CoreCryptoModule; public final fun provideCryptoContext (Lme/proton/core/crypto/common/keystore/KeyStoreCrypto;)Lme/proton/core/crypto/common/context/CryptoContext; public final fun provideKeyStoreCrypto ()Lme/proton/core/crypto/common/keystore/KeyStoreCrypto; + public final fun provideSrpChallenge ()Lme/proton/core/crypto/common/srp/SrpChallenge; public final fun provideSrpCrypto ()Lme/proton/core/crypto/common/srp/SrpCrypto; } @@ -32,6 +33,14 @@ public final class me/proton/core/crypto/dagger/CoreCryptoModule_ProvideKeyStore public static fun provideKeyStoreCrypto ()Lme/proton/core/crypto/common/keystore/KeyStoreCrypto; } +public final class me/proton/core/crypto/dagger/CoreCryptoModule_ProvideSrpChallengeFactory : dagger/internal/Factory { + public fun ()V + public static fun create ()Lme/proton/core/crypto/dagger/CoreCryptoModule_ProvideSrpChallengeFactory; + public synthetic fun get ()Ljava/lang/Object; + public fun get ()Lme/proton/core/crypto/common/srp/SrpChallenge; + public static fun provideSrpChallenge ()Lme/proton/core/crypto/common/srp/SrpChallenge; +} + public final class me/proton/core/crypto/dagger/CoreCryptoModule_ProvideSrpCryptoFactory : dagger/internal/Factory { public fun ()V public static fun create ()Lme/proton/core/crypto/dagger/CoreCryptoModule_ProvideSrpCryptoFactory; diff --git a/crypto/dagger/src/main/kotlin/me/proton/core/crypto/dagger/CoreCryptoModule.kt b/crypto/dagger/src/main/kotlin/me/proton/core/crypto/dagger/CoreCryptoModule.kt index 8e20b729e..bc458d59b 100644 --- a/crypto/dagger/src/main/kotlin/me/proton/core/crypto/dagger/CoreCryptoModule.kt +++ b/crypto/dagger/src/main/kotlin/me/proton/core/crypto/dagger/CoreCryptoModule.kt @@ -24,9 +24,11 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import me.proton.core.crypto.android.context.AndroidCryptoContext import me.proton.core.crypto.android.keystore.AndroidKeyStoreCrypto +import me.proton.core.crypto.android.srp.GOpenPGPSrpChallenge import me.proton.core.crypto.android.srp.GOpenPGPSrpCrypto import me.proton.core.crypto.common.context.CryptoContext import me.proton.core.crypto.common.keystore.KeyStoreCrypto +import me.proton.core.crypto.common.srp.SrpChallenge import me.proton.core.crypto.common.srp.SrpCrypto import javax.inject.Singleton @@ -49,4 +51,8 @@ public object CoreCryptoModule { @Provides @Singleton public fun provideSrpCrypto(): SrpCrypto = GOpenPGPSrpCrypto() + + @Provides + @Singleton + public fun provideSrpChallenge(): SrpChallenge = GOpenPGPSrpChallenge() } diff --git a/human-verification/dagger/api/human-verification-dagger.api b/human-verification/dagger/api/human-verification-dagger.api index 9268b60cc..879b523a0 100644 --- a/human-verification/dagger/api/human-verification-dagger.api +++ b/human-verification/dagger/api/human-verification-dagger.api @@ -12,6 +12,8 @@ public final class me/proton/core/humanverification/dagger/BuildConfig { public abstract interface class me/proton/core/humanverification/dagger/CoreHumanVerificationModule { public abstract fun bindHumanVerificationManager (Lme/proton/core/humanverification/data/HumanVerificationManagerImpl;)Lme/proton/core/humanverification/domain/HumanVerificationManager; public abstract fun bindHumanVerificationWorkflowHandler (Lme/proton/core/humanverification/data/HumanVerificationManagerImpl;)Lme/proton/core/humanverification/domain/HumanVerificationWorkflowHandler; + public abstract fun provideDeviceVerificationListener (Lme/proton/core/humanverification/data/DeviceVerificationListenerImpl;)Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener; + public abstract fun provideDeviceVerificationProvider (Lme/proton/core/humanverification/data/DeviceVerificationProviderImpl;)Lme/proton/core/network/domain/deviceverification/DeviceVerificationProvider; public abstract fun provideHumanVerificationConfiguration (Lme/proton/core/humanverification/domain/HumanVerificationExternalInputImpl;)Lme/proton/core/humanverification/domain/HumanVerificationExternalInput; public abstract fun provideHumanVerificationListener (Lme/proton/core/humanverification/data/HumanVerificationListenerImpl;)Lme/proton/core/network/domain/humanverification/HumanVerificationListener; public abstract fun provideHumanVerificationProvider (Lme/proton/core/humanverification/data/HumanVerificationProviderImpl;)Lme/proton/core/network/domain/humanverification/HumanVerificationProvider; diff --git a/human-verification/dagger/src/main/kotlin/me/proton/core/humanverification/dagger/CoreHumanVerificationModule.kt b/human-verification/dagger/src/main/kotlin/me/proton/core/humanverification/dagger/CoreHumanVerificationModule.kt index 1e18c21fd..a2b996772 100644 --- a/human-verification/dagger/src/main/kotlin/me/proton/core/humanverification/dagger/CoreHumanVerificationModule.kt +++ b/human-verification/dagger/src/main/kotlin/me/proton/core/humanverification/dagger/CoreHumanVerificationModule.kt @@ -22,6 +22,8 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import me.proton.core.humanverification.data.DeviceVerificationListenerImpl +import me.proton.core.humanverification.data.DeviceVerificationProviderImpl import me.proton.core.humanverification.data.HumanVerificationListenerImpl import me.proton.core.humanverification.data.HumanVerificationManagerImpl import me.proton.core.humanverification.data.HumanVerificationProviderImpl @@ -33,6 +35,8 @@ import me.proton.core.humanverification.domain.HumanVerificationManager import me.proton.core.humanverification.domain.HumanVerificationWorkflowHandler import me.proton.core.humanverification.domain.repository.HumanVerificationRepository import me.proton.core.humanverification.domain.utils.NetworkRequestOverrider +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider import javax.inject.Singleton @@ -56,6 +60,14 @@ public interface CoreHumanVerificationModule { @Singleton public fun provideHumanVerificationProvider(impl: HumanVerificationProviderImpl): HumanVerificationProvider + @Binds + @Singleton + public fun provideDeviceVerificationProvider(impl: DeviceVerificationProviderImpl): DeviceVerificationProvider + + @Binds + @Singleton + public fun provideDeviceVerificationListener(impl: DeviceVerificationListenerImpl): DeviceVerificationListener + @Binds @Singleton public fun provideHumanVerificationRepository(impl: HumanVerificationRepositoryImpl): HumanVerificationRepository diff --git a/human-verification/data/api/human-verification-data.api b/human-verification/data/api/human-verification-data.api index 94bd4a56a..7cf075908 100644 --- a/human-verification/data/api/human-verification-data.api +++ b/human-verification/data/api/human-verification-data.api @@ -5,6 +5,18 @@ public final class me/proton/core/humanverification/data/BuildConfig { public fun ()V } +public final class me/proton/core/humanverification/data/DeviceVerificationListenerImpl : me/proton/core/network/domain/deviceverification/DeviceVerificationListener { + public fun (Lme/proton/core/network/domain/deviceverification/DeviceVerificationProvider;Lme/proton/core/crypto/common/srp/SrpChallenge;)V + public fun onDeviceVerification (Lme/proton/core/network/domain/session/SessionId;Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class me/proton/core/humanverification/data/DeviceVerificationProviderImpl : me/proton/core/network/domain/deviceverification/DeviceVerificationProvider { + public fun ()V + public fun getSolvedChallenge (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun getSolvedChallenge (Lme/proton/core/network/domain/session/SessionId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun setSolvedChallenge (Lme/proton/core/network/domain/session/SessionId;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public class me/proton/core/humanverification/data/HumanVerificationListenerImpl : me/proton/core/network/domain/humanverification/HumanVerificationListener { public fun (Lme/proton/core/humanverification/domain/repository/HumanVerificationRepository;)V public fun onHumanVerificationInvalid (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -29,6 +41,12 @@ public final class me/proton/core/humanverification/data/HumanVerificationProvid public fun getHumanVerificationDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class me/proton/core/humanverification/data/LogTag { + public static final field DEFAULT Ljava/lang/String; + public static final field INSTANCE Lme/proton/core/humanverification/data/LogTag; + public static final field SRP_CHALLENGE_ERROR Ljava/lang/String; +} + public final class me/proton/core/humanverification/data/db/HumanVerificationConverters { public fun ()V public final fun fromClientIdTypeToString (Lme/proton/core/network/domain/client/ClientIdType;)Ljava/lang/String; diff --git a/human-verification/data/build.gradle.kts b/human-verification/data/build.gradle.kts index fae0e273f..894293d60 100644 --- a/human-verification/data/build.gradle.kts +++ b/human-verification/data/build.gradle.kts @@ -45,9 +45,11 @@ dependencies { implementation( project(Module.kotlinUtil), + project(Module.cryptoCommon), `coroutines-core`, `okHttp-logging`, - `room-ktx` + `room-ktx`, + cache4k ) testImplementation( diff --git a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/DeviceVerificationListenerImpl.kt b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/DeviceVerificationListenerImpl.kt new file mode 100644 index 000000000..314ab3cd0 --- /dev/null +++ b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/DeviceVerificationListenerImpl.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.humanverification.data + +import me.proton.core.crypto.common.srp.SrpChallenge +import me.proton.core.network.domain.deviceverification.ChallengeType.Argon2 +import me.proton.core.network.domain.deviceverification.ChallengeType.Ecdlp +import me.proton.core.network.domain.deviceverification.ChallengeType.WASM +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener.DeviceVerificationResult +import me.proton.core.network.domain.deviceverification.DeviceVerificationMethods +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.session.SessionId +import me.proton.core.util.kotlin.CoreLogger +import javax.inject.Inject + +/** + * An implementation of [DeviceVerificationListener] that uses a [DeviceVerificationProvider] + * to handle device verification events. + * + * @property deviceVerificationProvider a data provider that cache the device verification details. + */ +class DeviceVerificationListenerImpl @Inject constructor( + private val deviceVerificationProvider: DeviceVerificationProvider, + private val srpChallenge: SrpChallenge, +) : DeviceVerificationListener { + + /** + * Called when a device verification workflow is needed. + * This method suspends the current coroutine until the verification is completed. + * + * @param methods a [DeviceVerificationMethods] object that contains the challenge type and payload. + * @return a [DeviceVerificationResult] indicating whether the verification was successful or failed. + */ + override suspend fun onDeviceVerification( + sessionId: SessionId, + methods: DeviceVerificationMethods + ): DeviceVerificationResult { + // Try to get solved challenge from cache through provider + val cached = deviceVerificationProvider.getSolvedChallenge(methods.challengePayload) + if (cached != null) { + deviceVerificationProvider.setSolvedChallenge(sessionId, methods.challengePayload, cached) + return DeviceVerificationResult.Success + } + + // Solve the challenge. + val solvedChallenge = try { + when (methods.challengeType.enum) { + WASM -> srpChallenge.argon2PreimageChallenge(methods.challengePayload) + Ecdlp -> srpChallenge.ecdlpChallenge(methods.challengePayload) + Argon2 -> srpChallenge.argon2PreimageChallenge(methods.challengePayload) + null -> throw UnsupportedOperationException("Unsupported challenge type: ${methods.challengeType.value}.") + } + } catch (e: Exception) { + CoreLogger.e(LogTag.SRP_CHALLENGE_ERROR, e) + // If an exception occurs, return a failure result. + return DeviceVerificationResult.Failure + } + + // If the challenge is not solved, return a failure result. + if (solvedChallenge.isEmpty()) { + return DeviceVerificationResult.Failure + } + + // Use the deviceVerificationProvider to save the solved challenge + deviceVerificationProvider.setSolvedChallenge(sessionId, methods.challengePayload, solvedChallenge) + + return DeviceVerificationResult.Success + } +} \ No newline at end of file diff --git a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/DeviceVerificationProviderImpl.kt b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/DeviceVerificationProviderImpl.kt new file mode 100644 index 000000000..ac6c18ae9 --- /dev/null +++ b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/DeviceVerificationProviderImpl.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.humanverification.data + +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.session.SessionId +import javax.inject.Inject +import io.github.reactivecircus.cache4k.Cache +import kotlin.time.Duration.Companion.minutes + +/** + * An implementation of the DeviceVerificationProvider interface. + */ +class DeviceVerificationProviderImpl @Inject constructor() : DeviceVerificationProvider { + + // Cache for storing session IDs and their corresponding solved challenges. + private val sessionCache = Cache.Builder() + .expireAfterWrite(3.minutes) + .build() + + // Cache for storing challenge payloads and their corresponding solved challenges. + private val solvedCache = Cache.Builder() + .expireAfterWrite(3.minutes) + .build() + + /** + * Get the solved challenge associated with the given session ID. + * + * @param sessionId The session ID to look up. + * @return The solved challenge, or null if not found. + */ + override suspend fun getSolvedChallenge(sessionId: SessionId?): String? { + sessionId ?: return null + return sessionCache.get(sessionId) + } + + /** + * Get the solved challenge associated with the given challenge payload. + * + * @param challengePayload The challenge payload to look up. + * @return The solved challenge, or null if not found. + */ + override suspend fun getSolvedChallenge(challengePayload: String): String? { + return solvedCache.get(challengePayload) + } + + /** + * Set the solved challenge for the given session ID and challenge payload. + * + * @param sessionId The session ID to associate with the solved challenge. + * @param challengePayload The challenge payload to associate with the solved challenge. + * @param solved The solved challenge. + */ + override suspend fun setSolvedChallenge(sessionId: SessionId, challengePayload: String, solved: String) { + sessionCache.put(sessionId, solved) + solvedCache.put(challengePayload, solved) + } +} diff --git a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/LogTag.kt b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/LogTag.kt new file mode 100644 index 000000000..b9221a355 --- /dev/null +++ b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/LogTag.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.humanverification.data + +object LogTag { + /** Default tag for this module. */ + const val DEFAULT: String = "core.humanverification" + + const val SRP_CHALLENGE_ERROR = "core.humanverification.srp.challenge.error" +} \ No newline at end of file diff --git a/network/dagger/api/network-dagger.api b/network/dagger/api/network-dagger.api index c81674b87..c05278648 100644 --- a/network/dagger/api/network-dagger.api +++ b/network/dagger/api/network-dagger.api @@ -64,11 +64,11 @@ public final class me/proton/core/network/dagger/CoreNetworkModule { } public final class me/proton/core/network/dagger/CoreNetworkModule_ProvideApiFactory$network_dagger_releaseFactory : dagger/internal/Factory { - public fun (Lme/proton/core/network/dagger/CoreNetworkModule;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Lme/proton/core/network/dagger/CoreNetworkModule;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/network/dagger/CoreNetworkModule_ProvideApiFactory$network_dagger_releaseFactory; + public fun (Lme/proton/core/network/dagger/CoreNetworkModule;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Lme/proton/core/network/dagger/CoreNetworkModule;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/network/dagger/CoreNetworkModule_ProvideApiFactory$network_dagger_releaseFactory; public synthetic fun get ()Ljava/lang/Object; public fun get ()Lme/proton/core/network/data/ApiManagerFactory; - public static fun provideApiFactory$network_dagger_release (Lme/proton/core/network/dagger/CoreNetworkModule;Landroid/content/Context;Lme/proton/core/domain/entity/Product;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/data/ProtonCookieStore;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/client/ClientVersionValidator;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;Lokhttp3/HttpUrl;[Ljava/lang/String;[Ljava/lang/String;Ljava/util/List;Lokhttp3/OkHttpClient;)Lme/proton/core/network/data/ApiManagerFactory; + public static fun provideApiFactory$network_dagger_release (Lme/proton/core/network/dagger/CoreNetworkModule;Landroid/content/Context;Lme/proton/core/domain/entity/Product;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/data/ProtonCookieStore;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/deviceverification/DeviceVerificationProvider;Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/client/ClientVersionValidator;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;Lokhttp3/HttpUrl;[Ljava/lang/String;[Ljava/lang/String;Ljava/util/List;Lokhttp3/OkHttpClient;)Lme/proton/core/network/data/ApiManagerFactory; } public final class me/proton/core/network/dagger/CoreNetworkModule_ProvideCookieJar$network_dagger_releaseFactory : dagger/internal/Factory { diff --git a/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt b/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt index 83b54189a..a2b7f67b8 100644 --- a/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt +++ b/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt @@ -51,6 +51,8 @@ import me.proton.core.network.domain.UnAuthSessionsRepository import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.ExtraHeaderProvider +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider import me.proton.core.network.domain.scopes.MissingScopeListener @@ -86,6 +88,8 @@ public class CoreNetworkModule { sessionListener: SessionListener, humanVerificationProvider: HumanVerificationProvider, humanVerificationListener: HumanVerificationListener, + deviceVerificationProvider: DeviceVerificationProvider, + deviceVerificationListener: DeviceVerificationListener, missingScopeListener: MissingScopeListener, extraHeaderProvider: ExtraHeaderProvider, clientVersionValidator: ClientVersionValidator, @@ -109,6 +113,8 @@ public class CoreNetworkModule { sessionListener, humanVerificationProvider, humanVerificationListener, + deviceVerificationProvider, + deviceVerificationListener, missingScopeListener, cookieStore, CoroutineScope(Job() + Dispatchers.Default), diff --git a/network/data/api/network-data.api b/network/data/api/network-data.api index 7710ffa95..cf05dc171 100644 --- a/network/data/api/network-data.api +++ b/network/data/api/network-data.api @@ -1,6 +1,6 @@ public final class me/proton/core/network/data/ApiManagerFactory { - public fun (Landroid/content/Context;Lme/proton/core/domain/entity/Product;Lokhttp3/HttpUrl;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/client/ClientVersionValidator;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;[Ljava/lang/String;Lokhttp3/OkHttpClient;)V - public synthetic fun (Landroid/content/Context;Lme/proton/core/domain/entity/Product;Lokhttp3/HttpUrl;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/client/ClientVersionValidator;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;[Ljava/lang/String;Lokhttp3/OkHttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Lme/proton/core/domain/entity/Product;Lokhttp3/HttpUrl;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/deviceverification/DeviceVerificationProvider;Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/client/ClientVersionValidator;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;[Ljava/lang/String;Lokhttp3/OkHttpClient;)V + public synthetic fun (Landroid/content/Context;Lme/proton/core/domain/entity/Product;Lokhttp3/HttpUrl;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/deviceverification/DeviceVerificationProvider;Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/client/ClientVersionValidator;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;[Ljava/lang/String;Lokhttp3/OkHttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun create (Lme/proton/core/network/domain/session/SessionId;Lkotlin/reflect/KClass;Ljava/util/List;[Ljava/lang/String;Ljava/util/List;)Lme/proton/core/network/domain/ApiManager; public static synthetic fun create$default (Lme/proton/core/network/data/ApiManagerFactory;Lme/proton/core/network/domain/session/SessionId;Lkotlin/reflect/KClass;Ljava/util/List;[Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lme/proton/core/network/domain/ApiManager; public final fun getBaseOkHttpClient ()Lokhttp3/OkHttpClient; @@ -256,6 +256,7 @@ public final class me/proton/core/network/data/interceptor/ServerTimeInterceptor public final class me/proton/core/network/data/mapper/EntityMapperKt { public static final fun parseDetails (Lme/proton/core/network/domain/ApiResult$Error$ProtonData;ILme/proton/core/network/data/protonApi/Details;)Lme/proton/core/network/domain/ApiResult$Error$ProtonData; + public static final fun toDeviceVerificationEntity (Lme/proton/core/network/data/protonApi/Details;)Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods; public static final fun toHumanVerificationEntity (Lme/proton/core/network/data/protonApi/Details;)Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods; public static final fun toMissingScopes (Lme/proton/core/network/data/protonApi/Details;)Lme/proton/core/network/domain/scopes/MissingScopes; } @@ -269,15 +270,19 @@ public abstract interface class me/proton/core/network/data/protonApi/BaseRetrof public final class me/proton/core/network/data/protonApi/Details { public static final field Companion Lme/proton/core/network/data/protonApi/Details$Companion; public fun ()V - public synthetic fun (ILjava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (Ljava/util/List;Ljava/lang/String;Ljava/util/List;)V - public synthetic fun (Ljava/util/List;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILjava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/List; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/util/List; - public final fun copy (Ljava/util/List;Ljava/lang/String;Ljava/util/List;)Lme/proton/core/network/data/protonApi/Details; - public static synthetic fun copy$default (Lme/proton/core/network/data/protonApi/Details;Ljava/util/List;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lme/proton/core/network/data/protonApi/Details; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;)Lme/proton/core/network/data/protonApi/Details; + public static synthetic fun copy$default (Lme/proton/core/network/data/protonApi/Details;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/network/data/protonApi/Details; public fun equals (Ljava/lang/Object;)Z + public final fun getChallengePayload ()Ljava/lang/String; + public final fun getChallengeType ()Ljava/lang/Integer; public final fun getMissingScopes ()Ljava/util/List; public final fun getVerificationMethods ()Ljava/util/List; public final fun getVerificationToken ()Ljava/lang/String; diff --git a/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt b/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt index ee604a282..4fb0e12e4 100644 --- a/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt +++ b/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt @@ -39,6 +39,9 @@ import me.proton.core.network.domain.NetworkPrefs import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.ExtraHeaderProvider +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.handlers.DeviceVerificationNeededHandler import me.proton.core.network.domain.handlers.DohApiHandler import me.proton.core.network.domain.handlers.HumanVerificationInvalidHandler import me.proton.core.network.domain.handlers.HumanVerificationNeededHandler @@ -82,6 +85,8 @@ class ApiManagerFactory( private val sessionListener: SessionListener, private val humanVerificationProvider: HumanVerificationProvider, private val humanVerificationListener: HumanVerificationListener, + private val deviceVerificationProvider: DeviceVerificationProvider, + private val deviceVerificationListener: DeviceVerificationListener, private val missingScopeListener: MissingScopeListener, private val cookieStore: ProtonCookieStore, scope: CoroutineScope, @@ -144,6 +149,8 @@ class ApiManagerFactory( HumanVerificationNeededHandler(sessionId, clientIdProvider, humanVerificationListener, monoClockMs) val humanVerificationInvalidHandler = HumanVerificationInvalidHandler(sessionId, clientIdProvider, humanVerificationListener) + val deviceVerificationErrorHandler = + DeviceVerificationNeededHandler(sessionId, sessionProvider, deviceVerificationListener) return listOf( dohApiHandler, missingScopeHandler, @@ -151,6 +158,7 @@ class ApiManagerFactory( forceUpdateHandler, humanVerificationInvalidHandler, humanVerificationNeededHandler, + deviceVerificationErrorHandler, ) } @@ -187,6 +195,7 @@ class ApiManagerFactory( sessionId, sessionProvider, humanVerificationProvider, + deviceVerificationProvider, baseOkHttpClient, listOf(jsonConverter), interfaceClass, @@ -231,6 +240,7 @@ class ApiManagerFactory( sessionId, sessionProvider, humanVerificationProvider, + deviceVerificationProvider, baseOkHttpClient, listOf(jsonConverter), interfaceClass, diff --git a/network/data/src/main/kotlin/me/proton/core/network/data/ProtonApiBackend.kt b/network/data/src/main/kotlin/me/proton/core/network/data/ProtonApiBackend.kt index 732d6ad06..d307c33aa 100644 --- a/network/data/src/main/kotlin/me/proton/core/network/data/ProtonApiBackend.kt +++ b/network/data/src/main/kotlin/me/proton/core/network/data/ProtonApiBackend.kt @@ -38,6 +38,7 @@ import me.proton.core.network.domain.NetworkPrefs import me.proton.core.network.domain.TimeoutOverride import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ExtraHeaderProvider +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.humanverification.HumanVerificationProvider import me.proton.core.network.domain.server.ServerTimeListener import me.proton.core.network.domain.session.ResolvedSession @@ -81,6 +82,7 @@ internal class ProtonApiBackend( private val sessionId: SessionId?, private val sessionProvider: SessionProvider, private val humanVerificationProvider: HumanVerificationProvider, + private val deviceVerificationProvider: DeviceVerificationProvider, baseOkHttpClient: OkHttpClient, converters: List, interfaceClass: KClass, @@ -140,10 +142,12 @@ internal class ProtonApiBackend( .header("User-Agent", client.userAgent) .method(original.method, original.body) + // Set default Accept header if not present if (original.header("Accept") == null) { request.header("Accept", "application/vnd.protonmail.v1+json") } + // Add session-related headers when (val resolved = sessionProvider.getResolvedSession(sessionId)) { is ResolvedSession.Found -> { resolved.session.sessionId.id.takeIfNotBlank()?.let { uid -> @@ -156,6 +160,7 @@ internal class ProtonApiBackend( is ResolvedSession.NotFound -> Unit } + // Add human verification and device verification headers clientIdProvider.getClientId(sessionId)?.let { clientId -> humanVerificationProvider.getHumanVerificationDetails(clientId)?.let { details -> details.tokenType?.let { tokenType -> @@ -165,8 +170,13 @@ internal class ProtonApiBackend( request.header("x-pm-human-verification-token", tokenCode) } } + + deviceVerificationProvider.getSolvedChallenge(sessionId)?.let { solvedChallenge -> + request.header("x-pm-dv", solvedChallenge) + } } + // Add any additional headers extraHeaderProvider?.headers?.forEach { request.header(it.first, it.second) } diff --git a/network/data/src/main/kotlin/me/proton/core/network/data/mapper/EntityMapper.kt b/network/data/src/main/kotlin/me/proton/core/network/data/mapper/EntityMapper.kt index eb905aec4..661e85662 100644 --- a/network/data/src/main/kotlin/me/proton/core/network/data/mapper/EntityMapper.kt +++ b/network/data/src/main/kotlin/me/proton/core/network/data/mapper/EntityMapper.kt @@ -22,6 +22,8 @@ import me.proton.core.network.data.protonApi.Details import me.proton.core.network.domain.ApiResult import me.proton.core.network.domain.HttpResponseCodes import me.proton.core.network.domain.ResponseCodes +import me.proton.core.network.domain.deviceverification.ChallengeType +import me.proton.core.network.domain.deviceverification.DeviceVerificationMethods import me.proton.core.network.domain.humanverification.HumanVerificationAvailableMethods import me.proton.core.network.domain.scopes.MissingScopes import me.proton.core.network.domain.scopes.Scope @@ -36,6 +38,12 @@ fun Details.toHumanVerificationEntity(): HumanVerificationAvailableMethods = verificationToken = requireNotNull(verificationToken) ) +fun Details.toDeviceVerificationEntity(): DeviceVerificationMethods = + DeviceVerificationMethods( + challengeType = requireNotNull(challengeType?.let { ChallengeType.enumOf(it) }), + challengePayload = requireNotNull(challengePayload) + ) + fun Details.toMissingScopes(): MissingScopes = MissingScopes( scopes = missingScopes?.mapNotNull { @@ -48,6 +56,9 @@ fun ApiResult.Error.ProtonData.parseDetails(errorCode: Int, details: Details?): ResponseCodes.HUMAN_VERIFICATION_REQUIRED -> { humanVerification = details?.toHumanVerificationEntity() } + ResponseCodes.DEVICE_VERIFICATION_REQUIRED -> { + deviceVerification = details?.toDeviceVerificationEntity() + } HttpResponseCodes.HTTP_FORBIDDEN -> { missingScopes = details?.toMissingScopes() } diff --git a/network/data/src/main/kotlin/me/proton/core/network/data/protonApi/BaseRetrofitApi.kt b/network/data/src/main/kotlin/me/proton/core/network/data/protonApi/BaseRetrofitApi.kt index 9e4ce76b2..06eef59e9 100644 --- a/network/data/src/main/kotlin/me/proton/core/network/data/protonApi/BaseRetrofitApi.kt +++ b/network/data/src/main/kotlin/me/proton/core/network/data/protonApi/BaseRetrofitApi.kt @@ -63,5 +63,9 @@ data class Details( @SerialName("HumanVerificationToken") val verificationToken: String? = null, @SerialName("MissingScopes") - val missingScopes: List? = null + val missingScopes: List? = null, + @SerialName("ChallengeType") + val challengeType: Int? = null, + @SerialName("ChallengePayload") + val challengePayload: String? = null ) diff --git a/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt b/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt index d8810d271..56e07da10 100644 --- a/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt @@ -51,6 +51,8 @@ import me.proton.core.network.domain.ResponseCodes import me.proton.core.network.domain.client.ClientId import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ClientVersionValidator +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.handlers.DohApiHandler import me.proton.core.network.domain.handlers.TokenErrorHandler import me.proton.core.network.domain.humanverification.HumanVerificationListener @@ -97,6 +99,8 @@ internal class ApiManagerTests { ) private val humanVerificationProvider = mockk() private val humanVerificationListener = mockk() + private val deviceVerificationProvider = mockk() + private val deviceVerificationListener = mockk() private val missingScopeListener = mockk(relaxed = true) private lateinit var apiManagerFactory: ApiManagerFactory @@ -164,6 +168,8 @@ internal class ApiManagerTests { sessionListener, humanVerificationProvider, humanVerificationListener, + deviceVerificationProvider, + deviceVerificationListener, missingScopeListener, mockk(), testScope, diff --git a/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt b/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt index 54e8dfea1..044c81268 100644 --- a/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt @@ -47,6 +47,8 @@ import me.proton.core.network.domain.client.ClientId import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.CookieSessionId +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.humanverification.HumanVerificationDetails import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider @@ -102,7 +104,7 @@ internal class HumanVerificationTests { private val otherDetailsResponse = """ { - "Code": 9002, + "Code": 9003, "Error": "Something other happened", "ErrorDescription": "", "Details": { @@ -129,6 +131,8 @@ internal class HumanVerificationTests { private var sessionProvider = mockk() private val humanVerificationProvider = mockk() private val humanVerificationListener = mockk() + private val deviceVerificationProvider = mockk() + private val deviceVerificationListener = mockk() private val missingScopeListener = mockk(relaxed = true) private val clientVersionValidator = mockk { every { validate(any()) } returns true @@ -175,6 +179,8 @@ internal class HumanVerificationTests { sessionListener, humanVerificationProvider, humanVerificationListener, + deviceVerificationProvider, + deviceVerificationListener, missingScopeListener, cookieJar, scope, @@ -204,6 +210,7 @@ internal class HumanVerificationTests { sessionId, sessionProvider, humanVerificationProvider, + deviceVerificationProvider, apiManagerFactory.baseOkHttpClient, listOf( ScalarsConverterFactory.create(), @@ -239,6 +246,7 @@ internal class HumanVerificationTests { ) coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails + coEvery { deviceVerificationProvider.getSolvedChallenge(session.sessionId) } returns null val result = backend(ApiManager.Call(0) { test() }) assertTrue(result is ApiResult.Error.Http) @@ -261,6 +269,7 @@ internal class HumanVerificationTests { testTlsHelper.initPinning(it, TestTLSHelper.TEST_PINS) } + coEvery { deviceVerificationProvider.getSolvedChallenge(null) } returns null webServer.prepareResponse( 422, humanVerificationResponse @@ -307,6 +316,8 @@ internal class HumanVerificationTests { ) coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails + coEvery { deviceVerificationProvider.getSolvedChallenge(session.sessionId) } returns null + val result = backend(ApiManager.Call(0) { test() }) assertTrue(result is ApiResult.Error.Http) val data = result.proton @@ -332,6 +343,7 @@ internal class HumanVerificationTests { ) coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails + coEvery { deviceVerificationProvider.getSolvedChallenge(session.sessionId) } returns null backend(ApiManager.Call(0) { test() }) val headers = webServer.takeRequestWithDefaultTimeout()?.headers @@ -371,6 +383,7 @@ internal class HumanVerificationTests { ) coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails + coEvery { deviceVerificationProvider.getSolvedChallenge(null) } returns null backend(ApiManager.Call(0) { test() }) val headers = webServer.takeRequestWithDefaultTimeout()?.headers diff --git a/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt b/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt index 39a7963f9..1b6f2a230 100644 --- a/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt @@ -56,6 +56,8 @@ import me.proton.core.network.domain.ApiClient import me.proton.core.network.domain.ApiResult import me.proton.core.network.domain.TimeoutOverride import me.proton.core.network.domain.client.ExtraHeaderProvider +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider import me.proton.core.network.domain.scopes.MissingScopeListener @@ -104,6 +106,12 @@ internal class PinningTests { @BindValue internal val humanVerificationListener: HumanVerificationListener = mockk() + @BindValue + internal val deviceVerificationProvider: DeviceVerificationProvider = mockk() + + @BindValue + internal val deviceVerificationListener: DeviceVerificationListener = mockk() + @BindValue internal val humanVerificationProvider: HumanVerificationProvider = mockk() diff --git a/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt b/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt index a72563a61..44afc6cc7 100644 --- a/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt @@ -45,6 +45,8 @@ import me.proton.core.network.domain.client.ClientId import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.ExtraHeaderProvider +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider import me.proton.core.network.domain.humanverification.HumanVerificationDetails import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider @@ -92,6 +94,8 @@ internal class ProtonApiBackendTests { private val sessionProvider = mockk() private val humanVerificationProvider = mockk() private val humanVerificationListener = mockk() + private val deviceVerificationProvider = mockk() + private val deviceVerificationListener = mockk() private val missingScopeListener = mockk(relaxed = true) private val clientVersionValidator = mockk { every { validate(any()) } returns true @@ -137,6 +141,8 @@ internal class ProtonApiBackendTests { sessionListener, humanVerificationProvider, humanVerificationListener, + deviceVerificationProvider, + deviceVerificationListener, missingScopeListener, cookieJar, scope, @@ -167,6 +173,7 @@ internal class ProtonApiBackendTests { ) coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails + coEvery { deviceVerificationProvider.getSolvedChallenge(session.sessionId) } returns null } private fun createBackend(pinningInit: (OkHttpClient.Builder) -> Unit) = @@ -180,6 +187,7 @@ internal class ProtonApiBackendTests { session.sessionId, sessionProvider, humanVerificationProvider, + deviceVerificationProvider, apiManagerFactory.baseOkHttpClient, listOf( ScalarsConverterFactory.create(), diff --git a/network/domain/api/network-domain.api b/network/domain/api/network-domain.api index 66aa83b64..1654d8cae 100644 --- a/network/domain/api/network-domain.api +++ b/network/domain/api/network-domain.api @@ -131,20 +131,23 @@ public final class me/proton/core/network/domain/ApiResult$Error$Parse : me/prot } public final class me/proton/core/network/domain/ApiResult$Error$ProtonData { - public fun (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;)V - public synthetic fun (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;)V + public synthetic fun (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()I public final fun component2 ()Ljava/lang/String; public final fun component3 ()Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods; public final fun component4 ()Lme/proton/core/network/domain/scopes/MissingScopes; - public final fun copy (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;)Lme/proton/core/network/domain/ApiResult$Error$ProtonData; - public static synthetic fun copy$default (Lme/proton/core/network/domain/ApiResult$Error$ProtonData;ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;ILjava/lang/Object;)Lme/proton/core/network/domain/ApiResult$Error$ProtonData; + public final fun component5 ()Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods; + public final fun copy (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;)Lme/proton/core/network/domain/ApiResult$Error$ProtonData; + public static synthetic fun copy$default (Lme/proton/core/network/domain/ApiResult$Error$ProtonData;ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;ILjava/lang/Object;)Lme/proton/core/network/domain/ApiResult$Error$ProtonData; public fun equals (Ljava/lang/Object;)Z public final fun getCode ()I + public final fun getDeviceVerification ()Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods; public final fun getError ()Ljava/lang/String; public final fun getHumanVerification ()Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods; public final fun getMissingScopes ()Lme/proton/core/network/domain/scopes/MissingScopes; public fun hashCode ()I + public final fun setDeviceVerification (Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;)V public final fun setHumanVerification (Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;)V public final fun setMissingScopes (Lme/proton/core/network/domain/scopes/MissingScopes;)V public fun toString ()Ljava/lang/String; @@ -249,6 +252,7 @@ public final class me/proton/core/network/domain/ResponseCodes { public static final field API_VERSION_INVALID I public static final field APP_VERSION_BAD I public static final field APP_VERSION_NOT_SUPPORTED_FOR_EXTERNAL_ACCOUNTS I + public static final field DEVICE_VERIFICATION_REQUIRED I public static final field HUMAN_VERIFICATION_REQUIRED I public static final field INSTANCE Lme/proton/core/network/domain/ResponseCodes; public static final field NOT_ALLOWED I @@ -359,6 +363,65 @@ public abstract interface class me/proton/core/network/domain/client/ExtraHeader public abstract fun removeFirst (Ljava/lang/String;)V } +public final class me/proton/core/network/domain/deviceverification/ChallengeType : java/lang/Enum { + public static final field Argon2 Lme/proton/core/network/domain/deviceverification/ChallengeType; + public static final field Companion Lme/proton/core/network/domain/deviceverification/ChallengeType$Companion; + public static final field Ecdlp Lme/proton/core/network/domain/deviceverification/ChallengeType; + public static final field WASM Lme/proton/core/network/domain/deviceverification/ChallengeType; + public final fun getValue ()I + public static fun valueOf (Ljava/lang/String;)Lme/proton/core/network/domain/deviceverification/ChallengeType; + public static fun values ()[Lme/proton/core/network/domain/deviceverification/ChallengeType; +} + +public final class me/proton/core/network/domain/deviceverification/ChallengeType$Companion { + public final fun enumOf (I)Lme/proton/core/domain/type/IntEnum; + public final fun getMap ()Ljava/util/Map; +} + +public abstract interface class me/proton/core/network/domain/deviceverification/DeviceVerificationListener { + public abstract fun onDeviceVerification (Lme/proton/core/network/domain/session/SessionId;Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract class me/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult { +} + +public final class me/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult$Failure : me/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult { + public static final field INSTANCE Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult$Failure; +} + +public final class me/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult$Success : me/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult { + public static final field INSTANCE Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener$DeviceVerificationResult$Success; +} + +public final class me/proton/core/network/domain/deviceverification/DeviceVerificationMethods { + public fun (Lme/proton/core/domain/type/IntEnum;Ljava/lang/String;)V + public final fun component1 ()Lme/proton/core/domain/type/IntEnum; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Lme/proton/core/domain/type/IntEnum;Ljava/lang/String;)Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods; + public static synthetic fun copy$default (Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods;Lme/proton/core/domain/type/IntEnum;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/network/domain/deviceverification/DeviceVerificationMethods; + public fun equals (Ljava/lang/Object;)Z + public final fun getChallengePayload ()Ljava/lang/String; + public final fun getChallengeType ()Lme/proton/core/domain/type/IntEnum; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class me/proton/core/network/domain/deviceverification/DeviceVerificationProvider { + public abstract fun getSolvedChallenge (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getSolvedChallenge (Lme/proton/core/network/domain/session/SessionId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun setSolvedChallenge (Lme/proton/core/network/domain/session/SessionId;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class me/proton/core/network/domain/handlers/DeviceVerificationNeededHandler : me/proton/core/network/domain/ApiErrorHandler { + public static final field Companion Lme/proton/core/network/domain/handlers/DeviceVerificationNeededHandler$Companion; + public fun (Lme/proton/core/network/domain/session/SessionId;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/deviceverification/DeviceVerificationListener;)V + public fun invoke (Lme/proton/core/network/domain/ApiBackend;Lme/proton/core/network/domain/ApiResult$Error;Lme/proton/core/network/domain/ApiManager$Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class me/proton/core/network/domain/handlers/DeviceVerificationNeededHandler$Companion { + public final fun sessionMutex (Lme/proton/core/network/domain/session/SessionId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class me/proton/core/network/domain/handlers/DohApiHandler : me/proton/core/network/domain/ApiErrorHandler { public static final field Companion Lme/proton/core/network/domain/handlers/DohApiHandler$Companion; public fun (Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/ApiBackend;Lme/proton/core/network/domain/DohProvider;Lme/proton/core/network/domain/NetworkPrefs;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/serverconnection/DohAlternativesListener;Lkotlin/jvm/functions/Function1;)V diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/ApiResult.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/ApiResult.kt index 3ad3e44fc..bb1dd1a64 100644 --- a/network/domain/src/main/kotlin/me/proton/core/network/domain/ApiResult.kt +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/ApiResult.kt @@ -18,8 +18,7 @@ package me.proton.core.network.domain -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.withTimeoutOrNull +import me.proton.core.network.domain.deviceverification.DeviceVerificationMethods import me.proton.core.network.domain.humanverification.HumanVerificationAvailableMethods import me.proton.core.network.domain.scopes.MissingScopes import kotlin.time.Duration @@ -72,7 +71,8 @@ sealed class ApiResult { val code: Int, val error: String, var humanVerification: HumanVerificationAvailableMethods? = null, - var missingScopes: MissingScopes? = null + var missingScopes: MissingScopes? = null, + var deviceVerification: DeviceVerificationMethods? = null, ) /** diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt index 46bacf9bd..ab051229f 100644 --- a/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt @@ -30,6 +30,7 @@ object ResponseCodes { const val APP_VERSION_NOT_SUPPORTED_FOR_EXTERNAL_ACCOUNTS = 5099 const val PASSWORD_WRONG = 8002 const val HUMAN_VERIFICATION_REQUIRED = 9001 + const val DEVICE_VERIFICATION_REQUIRED = 9002 // new error code for device verification const val USER_CREATE_NAME_INVALID = 12_081 const val USER_CREATE_TOKEN_INVALID = 12_087 const val PAYMENTS_SUBSCRIPTION_NOT_EXISTS = 22_110 diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationListener.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationListener.kt new file mode 100644 index 000000000..52f3f8414 --- /dev/null +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationListener.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain.deviceverification + +import me.proton.core.network.domain.session.SessionId + +/** + * An interface for a listener that handles device verification events. + */ +interface DeviceVerificationListener { + + /** + * A sealed class that represents the result of a device verification operation. + * This class is used to indicate whether the verification was successful or failed. + */ + sealed class DeviceVerificationResult { + object Success : DeviceVerificationResult() + object Failure : DeviceVerificationResult() + } + + /** + * Called when a device verification workflow is needed. + * This method suspends the current coroutine until the verification is completed. + * + * @param methods a [DeviceVerificationMethods] object that contains the challenge type and payload. + * @return a [DeviceVerificationResult] indicating whether the verification was successful or failed. + */ + suspend fun onDeviceVerification(sessionId: SessionId, methods: DeviceVerificationMethods): DeviceVerificationResult +} \ No newline at end of file diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationMethods.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationMethods.kt new file mode 100644 index 000000000..1bbc064c4 --- /dev/null +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationMethods.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain.deviceverification + +import me.proton.core.domain.type.IntEnum + +/** + * A data class that represents the available device verification methods. + * + * @property challengeType an integer that represents the type of challenge. + * @property challengePayload a string that contains the challenge payload. + */ +data class DeviceVerificationMethods( + val challengeType: IntEnum, + val challengePayload: String +) + +public enum class ChallengeType(public val value: Int) { + WASM(1), + Argon2(2), + Ecdlp(3); + companion object { + val map = values().associateBy { it.value } + fun enumOf(value: Int) = IntEnum(value, map[value]) + } +} diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationProvider.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationProvider.kt new file mode 100644 index 000000000..6b0d0e9f4 --- /dev/null +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/deviceverification/DeviceVerificationProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain.deviceverification + +import me.proton.core.network.domain.session.SessionId + +interface DeviceVerificationProvider { + + /** + * Get the solved challenge associated with the given session ID. + * + * @param sessionId The session ID to look up. + * @return The solved challenge, or null if not found. + */ + suspend fun getSolvedChallenge(sessionId: SessionId?): String? + + /** + * Get the solved challenge associated with the given challenge payload. + * + * @param challengePayload The challenge payload to look up. + * @return The solved challenge, or null if not found. + */ + suspend fun getSolvedChallenge(challengePayload: String): String? + + /** + * Set the solved challenge for the given session ID and challenge payload. + * + * @param sessionId The session ID to associate with the solved challenge. + * @param challengePayload The challenge payload to associate with the solved challenge. + * @param solved The solved challenge. + */ + suspend fun setSolvedChallenge(sessionId: SessionId, challengePayload: String, solved: String) +} diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/DeviceVerificationNeededHandler.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/DeviceVerificationNeededHandler.kt new file mode 100644 index 000000000..c290bd8eb --- /dev/null +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/DeviceVerificationNeededHandler.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain.handlers + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import me.proton.core.network.domain.ApiBackend +import me.proton.core.network.domain.ApiErrorHandler +import me.proton.core.network.domain.ApiManager +import me.proton.core.network.domain.ApiResult +import me.proton.core.network.domain.ResponseCodes +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener.DeviceVerificationResult +import me.proton.core.network.domain.session.ResolvedSession +import me.proton.core.network.domain.session.SessionId +import me.proton.core.network.domain.session.SessionProvider +import me.proton.core.network.domain.session.getResolvedSession + +class DeviceVerificationNeededHandler( + private val sessionId: SessionId?, + private val sessionProvider: SessionProvider, + private val deviceVerificationListener: DeviceVerificationListener, +) : ApiErrorHandler { + + override suspend fun invoke( + backend: ApiBackend, + error: ApiResult.Error, + call: ApiManager.Call + ): ApiResult { + // Return the error if it's not a 9002 error + if (error !is ApiResult.Error.Http || error.proton?.code != ResponseCodes.DEVICE_VERIFICATION_REQUIRED) { + return error + } + // Return the error if there are no details in the error + val details = error.proton.deviceVerification ?: return error + + val sessionId = when (val resolvedSession = sessionProvider.getResolvedSession(sessionId)) { + is ResolvedSession.NotFound -> return error + is ResolvedSession.Found -> resolvedSession.session.sessionId + } + + // Allow only one coroutine at a time per sessionId + return sessionMutex(sessionId).withLock { + when (deviceVerificationListener.onDeviceVerification(sessionId, details)) { + is DeviceVerificationResult.Success -> backend(call) + is DeviceVerificationResult.Failure -> error + } + } + } + + companion object { + private val staticMutex: Mutex = Mutex() + private val sessionMutexMap: MutableMap = HashMap() + + suspend fun sessionMutex(sessionId: SessionId) = + staticMutex.withLock { sessionMutexMap.getOrPut(sessionId) { Mutex() } } + } +} + diff --git a/network/domain/src/test/kotlin/me/proton/core/network/domain/DeviceVerificationHandlerTest.kt b/network/domain/src/test/kotlin/me/proton/core/network/domain/DeviceVerificationHandlerTest.kt new file mode 100644 index 000000000..f4c1c4499 --- /dev/null +++ b/network/domain/src/test/kotlin/me/proton/core/network/domain/DeviceVerificationHandlerTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import me.proton.core.network.domain.deviceverification.ChallengeType +import me.proton.core.network.domain.deviceverification.DeviceVerificationListener +import me.proton.core.network.domain.deviceverification.DeviceVerificationMethods +import me.proton.core.network.domain.handlers.DeviceVerificationNeededHandler +import me.proton.core.network.domain.session.ResolvedSession +import me.proton.core.network.domain.session.Session +import me.proton.core.network.domain.session.SessionId +import me.proton.core.network.domain.session.SessionProvider +import me.proton.core.network.domain.session.getResolvedSession +import org.junit.Test +import kotlin.test.BeforeTest +import kotlin.test.assertNotNull + +/** + * Tests for Device Verification domain handler. + */ +class DeviceVerificationHandlerTest { + + private val sessionId = SessionId("sessionId") + private val session = Session( + sessionId, + "TokenOK", + "TokenOK", + listOf("ok1", "ok2"), + ) + private val sessionIdProvider = mockk() + private val deviceVerificationListener = mockk() + + private val apiBackend = mockk>() + + @BeforeTest + fun beforeTest() { + coEvery { sessionIdProvider.getResolvedSession(any()) } returns ResolvedSession.Found.Authenticated(session) + coEvery { sessionIdProvider.getSession(any()) } returns session + } + + @Test + fun `test device verification called`() = runTest { + val challengeType = ChallengeType.enumOf(1) + val deviceVerificationMethods = DeviceVerificationMethods(challengeType, "TestPayload") + val apiResult = ApiResult.Error.Http( + 422, + "Device Verification required", + ApiResult.Error.ProtonData( + 9002, + "Device Verification required", + null, + null, + deviceVerificationMethods + ) + ) + + coEvery { + deviceVerificationListener.onDeviceVerification( + sessionId, + any() + ) + } returns DeviceVerificationListener.DeviceVerificationResult.Success + + coEvery { apiBackend.invoke(any()) } returns ApiResult.Success("test") + + val deviceVerificationHandler = + DeviceVerificationNeededHandler(sessionId, sessionIdProvider, deviceVerificationListener) + + val result = deviceVerificationHandler.invoke( + backend = apiBackend, + error = apiResult, + call = mockk>() + ) + + assertNotNull(result) + coVerify(exactly = 1) { + deviceVerificationListener.onDeviceVerification(sessionId, deviceVerificationMethods) + } + } + + @Test + fun `test device verification called but retry api failed`() = runTest { + val challengeType = ChallengeType.enumOf(1) + val deviceVerificationMethods = DeviceVerificationMethods(challengeType, "TestPayload") + val apiResult = ApiResult.Error.Http( + 422, + "device Verification required", + ApiResult.Error.ProtonData( + 9002, + "device Verification required", + null, + null, + deviceVerificationMethods + ) + ) + + coEvery { + deviceVerificationListener.onDeviceVerification( + sessionId, + any() + ) + } returns DeviceVerificationListener.DeviceVerificationResult.Success + + coEvery { apiBackend.invoke(any()) } returns apiResult + + val deviceVerificationHandler = + DeviceVerificationNeededHandler(sessionId, sessionIdProvider, deviceVerificationListener) + val result = deviceVerificationHandler.invoke( + backend = apiBackend, + error = apiResult, + call = mockk>() + ) + + assertNotNull(result) + coVerify(exactly = 1) { + deviceVerificationListener.onDeviceVerification(sessionId, deviceVerificationMethods) + } + } + + @Test + fun `test device verification not called on other errors`() = runTest { + val apiResult = ApiResult.Error.Http( + 422, + "Some error", + ApiResult.Error.ProtonData( + 9000, + "Some error" + ) + ) + + val deviceVerificationHandler = + DeviceVerificationNeededHandler(sessionId, sessionIdProvider, deviceVerificationListener) + + val result = deviceVerificationHandler.invoke( + backend = mockk(), + error = apiResult, + call = mockk>() + ) + + assertNotNull(result) + coVerify(exactly = 0) { + deviceVerificationListener.onDeviceVerification(sessionId, any()) + } + } + + @Test + fun `test no proton data does not crash the handler`() = runTest { + val apiResult = ApiResult.Error.Http( + 422, + "Some error", + null + ) + + val deviceVerificationHandler = + DeviceVerificationNeededHandler(sessionId, sessionIdProvider, deviceVerificationListener) + + val result = deviceVerificationHandler.invoke( + backend = mockk(), + error = apiResult, + call = mockk>() + ) + + assertNotNull(result) + coVerify(exactly = 0) { + deviceVerificationListener.onDeviceVerification(sessionId, any()) + } + } + + @Test + fun `test connectivity error does not invoke device verification`() = runTest { + val apiResult = ApiResult.Error.Connection( + false + ) + + val deviceVerificationHandler = + DeviceVerificationNeededHandler(sessionId, sessionIdProvider, deviceVerificationListener) + + val result = deviceVerificationHandler.invoke( + backend = mockk(), + error = apiResult, + call = mockk>() + ) + + assertNotNull(result) + coVerify(exactly = 0) { + deviceVerificationListener.onDeviceVerification(sessionId, any()) + } + } +}