feat(human-verification): Add proof of work to the core library.

This commit is contained in:
yzhang
2023-04-14 03:21:07 -07:00
parent 9376f54c4b
commit c3fa5ce2d8
35 changed files with 881 additions and 29 deletions
@@ -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()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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=="
}
}
+6
View File
@@ -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 <init> ()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
@@ -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("") }
}
}
@@ -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)
}
}
+5
View File
@@ -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;
@@ -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;
}
+9
View File
@@ -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 <init> ()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 <init> ()V
public static fun create ()Lme/proton/core/crypto/dagger/CoreCryptoModule_ProvideSrpCryptoFactory;
@@ -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()
}
@@ -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;
@@ -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
@@ -5,6 +5,18 @@ public final class me/proton/core/humanverification/data/BuildConfig {
public fun <init> ()V
}
public final class me/proton/core/humanverification/data/DeviceVerificationListenerImpl : me/proton/core/network/domain/deviceverification/DeviceVerificationListener {
public fun <init> (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 <init> ()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 <init> (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 <init> ()V
public final fun fromClientIdTypeToString (Lme/proton/core/network/domain/client/ClientIdType;)Ljava/lang/String;
+3 -1
View File
@@ -45,9 +45,11 @@ dependencies {
implementation(
project(Module.kotlinUtil),
project(Module.cryptoCommon),
`coroutines-core`,
`okHttp-logging`,
`room-ktx`
`room-ktx`,
cache4k
)
testImplementation(
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SessionId, String>()
// Cache for storing challenge payloads and their corresponding solved challenges.
private val solvedCache = Cache.Builder()
.expireAfterWrite(3.minutes)
.build<String, String>()
/**
* 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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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"
}
+3 -3
View File
@@ -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 <init> (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 <init> (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 {
@@ -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),
+12 -7
View File
@@ -1,6 +1,6 @@
public final class me/proton/core/network/data/ApiManagerFactory {
public fun <init> (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 <init> (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 <init> (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 <init> (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 <init> ()V
public synthetic fun <init> (ILjava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/util/List;)V
public synthetic fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (ILjava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;)V
public synthetic fun <init> (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;
@@ -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<Api>(sessionId, clientIdProvider, humanVerificationListener, monoClockMs)
val humanVerificationInvalidHandler =
HumanVerificationInvalidHandler<Api>(sessionId, clientIdProvider, humanVerificationListener)
val deviceVerificationErrorHandler =
DeviceVerificationNeededHandler<Api>(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,
@@ -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<Api : BaseRetrofitApi>(
private val sessionId: SessionId?,
private val sessionProvider: SessionProvider,
private val humanVerificationProvider: HumanVerificationProvider,
private val deviceVerificationProvider: DeviceVerificationProvider,
baseOkHttpClient: OkHttpClient,
converters: List<Converter.Factory>,
interfaceClass: KClass<Api>,
@@ -140,10 +142,12 @@ internal class ProtonApiBackend<Api : BaseRetrofitApi>(
.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<Api : BaseRetrofitApi>(
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<Api : BaseRetrofitApi>(
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)
}
@@ -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()
}
@@ -63,5 +63,9 @@ data class Details(
@SerialName("HumanVerificationToken")
val verificationToken: String? = null,
@SerialName("MissingScopes")
val missingScopes: List<String>? = null
val missingScopes: List<String>? = null,
@SerialName("ChallengeType")
val challengeType: Int? = null,
@SerialName("ChallengePayload")
val challengePayload: String? = null
)
@@ -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<HumanVerificationProvider>()
private val humanVerificationListener = mockk<HumanVerificationListener>()
private val deviceVerificationProvider = mockk<DeviceVerificationProvider>()
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
private lateinit var apiManagerFactory: ApiManagerFactory
@@ -164,6 +168,8 @@ internal class ApiManagerTests {
sessionListener,
humanVerificationProvider,
humanVerificationListener,
deviceVerificationProvider,
deviceVerificationListener,
missingScopeListener,
mockk(),
testScope,
@@ -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<SessionProvider>()
private val humanVerificationProvider = mockk<HumanVerificationProvider>()
private val humanVerificationListener = mockk<HumanVerificationListener>()
private val deviceVerificationProvider = mockk<DeviceVerificationProvider>()
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
private val clientVersionValidator = mockk<ClientVersionValidator> {
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
@@ -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()
@@ -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<SessionProvider>()
private val humanVerificationProvider = mockk<HumanVerificationProvider>()
private val humanVerificationListener = mockk<HumanVerificationListener>()
private val deviceVerificationProvider = mockk<DeviceVerificationProvider>()
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
private val clientVersionValidator = mockk<ClientVersionValidator> {
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(),
+67 -4
View File
@@ -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 <init> (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;)V
public synthetic fun <init> (ILjava/lang/String;Lme/proton/core/network/domain/humanverification/HumanVerificationAvailableMethods;Lme/proton/core/network/domain/scopes/MissingScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (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 <init> (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 <init> (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 <init> (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 <init> (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
@@ -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<out T> {
val code: Int,
val error: String,
var humanVerification: HumanVerificationAvailableMethods? = null,
var missingScopes: MissingScopes? = null
var missingScopes: MissingScopes? = null,
var deviceVerification: DeviceVerificationMethods? = null,
)
/**
@@ -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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ChallengeType>,
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])
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Api>(
private val sessionId: SessionId?,
private val sessionProvider: SessionProvider,
private val deviceVerificationListener: DeviceVerificationListener,
) : ApiErrorHandler<Api> {
override suspend fun <T> invoke(
backend: ApiBackend<Api>,
error: ApiResult.Error,
call: ApiManager.Call<Api, T>
): ApiResult<T> {
// 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<SessionId, Mutex> = HashMap()
suspend fun sessionMutex(sessionId: SessionId) =
staticMutex.withLock { sessionMutexMap.getOrPut(sessionId) { Mutex() } }
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SessionProvider>()
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
private val apiBackend = mockk<ApiBackend<Any>>()
@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>(any()) } returns ApiResult.Success("test")
val deviceVerificationHandler =
DeviceVerificationNeededHandler<Any>(sessionId, sessionIdProvider, deviceVerificationListener)
val result = deviceVerificationHandler.invoke(
backend = apiBackend,
error = apiResult,
call = mockk<ApiManager.Call<Any, Any>>()
)
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>(any()) } returns apiResult
val deviceVerificationHandler =
DeviceVerificationNeededHandler<Any>(sessionId, sessionIdProvider, deviceVerificationListener)
val result = deviceVerificationHandler.invoke(
backend = apiBackend,
error = apiResult,
call = mockk<ApiManager.Call<Any, Any>>()
)
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<Any>(sessionId, sessionIdProvider, deviceVerificationListener)
val result = deviceVerificationHandler.invoke(
backend = mockk(),
error = apiResult,
call = mockk<ApiManager.Call<Any, Any>>()
)
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<Any>(sessionId, sessionIdProvider, deviceVerificationListener)
val result = deviceVerificationHandler.invoke(
backend = mockk(),
error = apiResult,
call = mockk<ApiManager.Call<Any, Any>>()
)
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<Any>(sessionId, sessionIdProvider, deviceVerificationListener)
val result = deviceVerificationHandler.invoke(
backend = mockk(),
error = apiResult,
call = mockk<ApiManager.Call<Any, Any>>()
)
assertNotNull(result)
coVerify(exactly = 0) {
deviceVerificationListener.onDeviceVerification(sessionId, any())
}
}
}