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())
+ }
+ }
+}