mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-06-14 09:54:49 +00:00
feat(human-verification): Add proof of work to the core library.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
+33
@@ -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=="
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+5
-4
@@ -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("") }
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+12
@@ -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;
|
||||
|
||||
@@ -45,9 +45,11 @@ dependencies {
|
||||
|
||||
implementation(
|
||||
project(Module.kotlinUtil),
|
||||
project(Module.cryptoCommon),
|
||||
`coroutines-core`,
|
||||
`okHttp-logging`,
|
||||
`room-ktx`
|
||||
`room-ktx`,
|
||||
cache4k
|
||||
)
|
||||
|
||||
testImplementation(
|
||||
|
||||
+86
@@ -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
|
||||
}
|
||||
}
|
||||
+74
@@ -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)
|
||||
}
|
||||
}
|
||||
+26
@@ -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"
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+5
-1
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
+45
@@ -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
|
||||
}
|
||||
+42
@@ -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])
|
||||
}
|
||||
}
|
||||
+49
@@ -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)
|
||||
}
|
||||
+75
@@ -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() } }
|
||||
}
|
||||
}
|
||||
|
||||
+209
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user