mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-05-15 09:50:41 +00:00
feat(auth, user): Support for second factor FIDO2 when obtaining password scope.
This commit is contained in:
committed by
MargeBot
parent
49efb7b352
commit
bf5a84eb86
@@ -157,6 +157,10 @@ public final class me/proton/core/auth/fido/domain/entity/Fido2RegisteredKey$Com
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
}
|
||||
|
||||
public final class me/proton/core/auth/fido/domain/ext/Fido2AuthenticationExtensionsClientInputsExtKt {
|
||||
public static final fun toJson (Lme/proton/core/auth/fido/domain/entity/Fido2AuthenticationExtensionsClientInputs;)Lkotlinx/serialization/json/JsonObject;
|
||||
}
|
||||
|
||||
public abstract interface class me/proton/core/auth/fido/domain/usecase/PerformTwoFaWithSecurityKey {
|
||||
public abstract fun invoke (Ljava/lang/Object;Lme/proton/core/auth/fido/domain/entity/Fido2PublicKeyCredentialRequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun register (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import studio.forface.easygradle.dsl.implementation
|
||||
import studio.forface.easygradle.dsl.*
|
||||
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
@@ -27,6 +27,7 @@ publishOption.shouldBePublishedAsLib = true
|
||||
|
||||
dependencies {
|
||||
implementation(
|
||||
`serialization-core`
|
||||
`serialization-core`,
|
||||
`serialization-json`
|
||||
)
|
||||
}
|
||||
|
||||
+2
-16
@@ -16,14 +16,13 @@
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.data.api.fido2
|
||||
package me.proton.core.auth.fido.domain.ext
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2AuthenticationExtensionsClientInputs
|
||||
|
||||
internal fun Fido2AuthenticationExtensionsClientInputs.toJson(): JsonObject? = JsonObject(
|
||||
public fun Fido2AuthenticationExtensionsClientInputs.toJson(): JsonObject? = JsonObject(
|
||||
buildMap {
|
||||
if (appId != null) {
|
||||
put("appid", JsonPrimitive(appId))
|
||||
@@ -36,16 +35,3 @@ internal fun Fido2AuthenticationExtensionsClientInputs.toJson(): JsonObject? = J
|
||||
}
|
||||
}
|
||||
).takeIf { it.isNotEmpty() }
|
||||
|
||||
internal fun PublicKeyCredentialRequestOptionsResponse.toFido2AuthenticationExtensionsClientInputs() =
|
||||
Fido2AuthenticationExtensionsClientInputs(
|
||||
appId = extensions?.get("appid")?.let { jsonElement ->
|
||||
(jsonElement as? JsonPrimitive)?.takeIf { it.isString }?.content?.takeIf { it.isNotEmpty() }
|
||||
},
|
||||
thirdPartyPayment = extensions?.get("thirdPartyPayment")?.let { jsonElement ->
|
||||
(jsonElement as? JsonPrimitive)?.booleanOrNull
|
||||
},
|
||||
uvm = extensions?.get("uvm")?.let { jsonElement ->
|
||||
(jsonElement as? JsonPrimitive)?.booleanOrNull
|
||||
},
|
||||
)
|
||||
@@ -29,8 +29,8 @@ protonBuild {
|
||||
}
|
||||
|
||||
protonCoverage {
|
||||
branchCoveragePercentage.set(67)
|
||||
lineCoveragePercentage.set(68)
|
||||
branchCoveragePercentage.set(64)
|
||||
lineCoveragePercentage.set(67)
|
||||
}
|
||||
|
||||
publishOption.shouldBePublishedAsLib = true
|
||||
|
||||
+17
@@ -20,6 +20,9 @@ package me.proton.core.auth.data.api.fido2
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2AuthenticationExtensionsClientInputs
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2PublicKeyCredentialRequestOptions
|
||||
|
||||
/**
|
||||
@@ -70,3 +73,17 @@ data class PublicKeyCredentialRequestOptionsResponse(
|
||||
extensions = toFido2AuthenticationExtensionsClientInputs()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun PublicKeyCredentialRequestOptionsResponse.toFido2AuthenticationExtensionsClientInputs() =
|
||||
Fido2AuthenticationExtensionsClientInputs(
|
||||
appId = extensions?.get("appid")?.let { jsonElement ->
|
||||
(jsonElement as? JsonPrimitive)?.takeIf { it.isString }?.content?.takeIf { it.isNotEmpty() }
|
||||
},
|
||||
thirdPartyPayment = extensions?.get("thirdPartyPayment")?.let { jsonElement ->
|
||||
(jsonElement as? JsonPrimitive)?.booleanOrNull
|
||||
},
|
||||
uvm = extensions?.get("uvm")?.let { jsonElement ->
|
||||
(jsonElement as? JsonPrimitive)?.booleanOrNull
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+25
-1
@@ -55,4 +55,28 @@ data class Fido2Request(
|
||||
val signature: String, // base64
|
||||
@SerialName("CredentialID")
|
||||
val credentialID: UByteArray
|
||||
)
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Fido2Request
|
||||
|
||||
if (authenticationOptions != other.authenticationOptions) return false
|
||||
if (clientData != other.clientData) return false
|
||||
if (authenticatorData != other.authenticatorData) return false
|
||||
if (signature != other.signature) return false
|
||||
if (credentialID != other.credentialID) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = authenticationOptions.hashCode()
|
||||
result = 31 * result + clientData.hashCode()
|
||||
result = 31 * result + authenticatorData.hashCode()
|
||||
result = 31 * result + signature.hashCode()
|
||||
result = 31 * result + credentialID.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import me.proton.core.auth.data.api.AuthenticationApi
|
||||
import me.proton.core.auth.data.api.fido2.AuthenticationOptionsData
|
||||
import me.proton.core.auth.data.api.fido2.PublicKeyCredentialDescriptorData
|
||||
import me.proton.core.auth.data.api.fido2.PublicKeyCredentialRequestOptionsResponse
|
||||
import me.proton.core.auth.data.api.fido2.toJson
|
||||
import me.proton.core.auth.fido.domain.ext.toJson
|
||||
import me.proton.core.auth.data.api.request.AuthInfoRequest
|
||||
import me.proton.core.auth.data.api.request.EmailValidationRequest
|
||||
import me.proton.core.auth.data.api.request.Fido2Request
|
||||
|
||||
+1
-31
@@ -3,11 +3,11 @@ package me.proton.core.auth.data.api.fido2
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2AuthenticationExtensionsClientInputs
|
||||
import me.proton.core.auth.fido.domain.ext.toJson
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
class Fido2AuthenticationExtensionsClientInputsExtTest {
|
||||
@Test
|
||||
fun `null client inputs to json`() {
|
||||
@@ -39,34 +39,4 @@ class Fido2AuthenticationExtensionsClientInputsExtTest {
|
||||
json
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request options response with null extensions to client inputs`() {
|
||||
val result = PublicKeyCredentialRequestOptionsResponse(
|
||||
challenge = ubyteArrayOf(1U, 2U, 3U),
|
||||
extensions = null
|
||||
).toFido2AuthenticationExtensionsClientInputs()
|
||||
|
||||
assertNull(result.appId)
|
||||
assertNull(result.thirdPartyPayment)
|
||||
assertNull(result.uvm)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request options response with extensions to client inputs`() {
|
||||
val result = PublicKeyCredentialRequestOptionsResponse(
|
||||
challenge = ubyteArrayOf(1U, 2U, 3U),
|
||||
extensions = JsonObject(
|
||||
mapOf(
|
||||
"appid" to JsonPrimitive("appId"),
|
||||
"thirdPartyPayment" to JsonPrimitive(true),
|
||||
"uvm" to JsonPrimitive(true)
|
||||
)
|
||||
)
|
||||
).toFido2AuthenticationExtensionsClientInputs()
|
||||
|
||||
assertEquals("appId", result.appId)
|
||||
assertEquals(true, result.thirdPartyPayment)
|
||||
assertEquals(true, result.uvm)
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package me.proton.core.auth.data.api.fido2
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
class PublicKeyCredentialRequestOptionsResponseKtTest {
|
||||
@Test
|
||||
fun `request options response with null extensions to client inputs`() {
|
||||
val result = PublicKeyCredentialRequestOptionsResponse(
|
||||
challenge = ubyteArrayOf(1U, 2U, 3U),
|
||||
extensions = null
|
||||
).toFido2AuthenticationExtensionsClientInputs()
|
||||
|
||||
assertNull(result.appId)
|
||||
assertNull(result.thirdPartyPayment)
|
||||
assertNull(result.uvm)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request options response with extensions to client inputs`() {
|
||||
val result = PublicKeyCredentialRequestOptionsResponse(
|
||||
challenge = ubyteArrayOf(1U, 2U, 3U),
|
||||
extensions = JsonObject(
|
||||
mapOf(
|
||||
"appid" to JsonPrimitive("appId"),
|
||||
"thirdPartyPayment" to JsonPrimitive(true),
|
||||
"uvm" to JsonPrimitive(true)
|
||||
)
|
||||
)
|
||||
).toFido2AuthenticationExtensionsClientInputs()
|
||||
|
||||
assertEquals("appId", result.appId)
|
||||
assertEquals(true, result.thirdPartyPayment)
|
||||
assertEquals(true, result.uvm)
|
||||
}
|
||||
}
|
||||
@@ -574,7 +574,7 @@ public final class me/proton/core/auth/domain/usecase/scopes/ObtainLockedScope {
|
||||
|
||||
public final class me/proton/core/auth/domain/usecase/scopes/ObtainPasswordScope {
|
||||
public fun <init> (Lme/proton/core/auth/domain/repository/AuthRepository;Lme/proton/core/user/domain/repository/UserRepository;Lme/proton/core/crypto/common/context/CryptoContext;)V
|
||||
public final fun invoke (Lme/proton/core/domain/entity/UserId;Lme/proton/core/network/domain/session/SessionId;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public final fun invoke (Lme/proton/core/domain/entity/UserId;Lme/proton/core/network/domain/session/SessionId;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/user/domain/entity/SecondFactorFido;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class me/proton/core/auth/domain/usecase/scopes/RemoveSecurityScopes {
|
||||
|
||||
+5
-2
@@ -25,6 +25,7 @@ import me.proton.core.crypto.common.keystore.decrypt
|
||||
import me.proton.core.crypto.common.keystore.use
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.network.domain.session.SessionId
|
||||
import me.proton.core.user.domain.entity.SecondFactorFido
|
||||
import me.proton.core.user.domain.repository.UserRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -38,7 +39,8 @@ class ObtainPasswordScope @Inject constructor(
|
||||
sessionId: SessionId,
|
||||
username: String,
|
||||
password: EncryptedString,
|
||||
twoFactorCode: String?
|
||||
secondFactorCode: String?,
|
||||
secondFactorFido: SecondFactorFido?
|
||||
): Boolean {
|
||||
val authInfo = authRepository.getAuthInfoSrp(
|
||||
sessionId = sessionId,
|
||||
@@ -57,7 +59,8 @@ class ObtainPasswordScope @Inject constructor(
|
||||
userId,
|
||||
clientProofs,
|
||||
authInfo.srpSession,
|
||||
twoFactorCode
|
||||
secondFactorCode = secondFactorCode,
|
||||
secondFactorFido = secondFactorFido
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-7
@@ -102,6 +102,7 @@ class ObtainPasswordScopeTest {
|
||||
testUserId,
|
||||
testSrpProofs,
|
||||
authInfoResult.srpSession,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} returns true
|
||||
@@ -110,7 +111,7 @@ class ObtainPasswordScopeTest {
|
||||
|
||||
@Test
|
||||
fun testUnlockingPasswordNo2FASuccess() = runTest {
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, null)
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, null, null)
|
||||
|
||||
coVerify { authRepository.getAuthInfoSrp(testSessionId, testUsername) }
|
||||
assertTrue(result)
|
||||
@@ -123,10 +124,11 @@ class ObtainPasswordScopeTest {
|
||||
testUserId,
|
||||
testSrpProofs,
|
||||
authInfoResult.srpSession,
|
||||
test2FACode
|
||||
test2FACode,
|
||||
null
|
||||
)
|
||||
} returns true
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, test2FACode)
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, test2FACode, null)
|
||||
|
||||
coVerify { authRepository.getAuthInfoSrp(testSessionId, testUsername) }
|
||||
assertTrue(result)
|
||||
@@ -139,10 +141,11 @@ class ObtainPasswordScopeTest {
|
||||
testUserId,
|
||||
testSrpProofs,
|
||||
authInfoResult.srpSession,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} returns false
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, null)
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, null, null)
|
||||
|
||||
coVerify { authRepository.getAuthInfoSrp(testSessionId, testUsername) }
|
||||
assertFalse(result)
|
||||
@@ -155,10 +158,11 @@ class ObtainPasswordScopeTest {
|
||||
testUserId,
|
||||
testSrpProofs,
|
||||
authInfoResult.srpSession,
|
||||
test2FACode
|
||||
test2FACode,
|
||||
null
|
||||
)
|
||||
} returns false
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, test2FACode)
|
||||
val result = useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, test2FACode, null)
|
||||
|
||||
coVerify { authRepository.getAuthInfoSrp(testSessionId, testUsername) }
|
||||
assertFalse(result)
|
||||
@@ -171,6 +175,7 @@ class ObtainPasswordScopeTest {
|
||||
testUserId,
|
||||
testSrpProofs,
|
||||
authInfoResult.srpSession,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} throws ApiException(
|
||||
@@ -183,7 +188,7 @@ class ObtainPasswordScopeTest {
|
||||
|
||||
// WHEN
|
||||
val throwable = assertFailsWith(ApiException::class) {
|
||||
useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, null)
|
||||
useCase.invoke(testUserId, testSessionId, testUsername, testPasswordEncrypted, null, null)
|
||||
}
|
||||
|
||||
// THEN
|
||||
|
||||
@@ -442,9 +442,13 @@ public final class me/proton/core/auth/presentation/alert/confirmpass/ConfirmPas
|
||||
public static final field BUNDLE_CONFIRM_PASS_DATA Ljava/lang/String;
|
||||
public static final field CONFIRM_PASS_SET Ljava/lang/String;
|
||||
public static final field Companion Lme/proton/core/auth/presentation/alert/confirmpass/ConfirmPasswordDialog$Companion;
|
||||
public field performTwoFaWithSecurityKey Ljava/util/Optional;
|
||||
public fun <init> ()V
|
||||
public final fun getPerformTwoFaWithSecurityKey ()Ljava/util/Optional;
|
||||
public fun onCreate (Landroid/os/Bundle;)V
|
||||
public fun onCreateDialog (Landroid/os/Bundle;)Landroid/app/Dialog;
|
||||
public fun onDismiss (Landroid/content/DialogInterface;)V
|
||||
public final fun setPerformTwoFaWithSecurityKey (Ljava/util/Optional;)V
|
||||
}
|
||||
|
||||
public final class me/proton/core/auth/presentation/alert/confirmpass/ConfirmPasswordDialog$Companion {
|
||||
@@ -455,6 +459,14 @@ public abstract interface class me/proton/core/auth/presentation/alert/confirmpa
|
||||
public abstract fun injectConfirmPasswordDialog (Lme/proton/core/auth/presentation/alert/confirmpass/ConfirmPasswordDialog;)V
|
||||
}
|
||||
|
||||
public final class me/proton/core/auth/presentation/alert/confirmpass/ConfirmPasswordDialog_MembersInjector : dagger/MembersInjector {
|
||||
public fun <init> (Ljavax/inject/Provider;)V
|
||||
public static fun create (Ljavax/inject/Provider;)Ldagger/MembersInjector;
|
||||
public synthetic fun injectMembers (Ljava/lang/Object;)V
|
||||
public fun injectMembers (Lme/proton/core/auth/presentation/alert/confirmpass/ConfirmPasswordDialog;)V
|
||||
public static fun injectPerformTwoFaWithSecurityKey (Lme/proton/core/auth/presentation/alert/confirmpass/ConfirmPasswordDialog;Ljava/util/Optional;)V
|
||||
}
|
||||
|
||||
public abstract class me/proton/core/auth/presentation/alert/confirmpass/Hilt_ConfirmPasswordDialog : androidx/fragment/app/DialogFragment, dagger/hilt/internal/GeneratedComponentManagerHolder {
|
||||
public final fun componentManager ()Ldagger/hilt/android/internal/managers/FragmentComponentManager;
|
||||
public synthetic fun componentManager ()Ldagger/hilt/internal/GeneratedComponentManager;
|
||||
@@ -2467,7 +2479,8 @@ public final class me/proton/core/auth/presentation/viewmodel/ConfirmPasswordDia
|
||||
public final fun getState ()Lkotlinx/coroutines/flow/SharedFlow;
|
||||
public final fun onConfirmPasswordResult (Lme/proton/core/network/domain/scopes/MissingScopeState;)Lkotlinx/coroutines/Job;
|
||||
public final fun setFido2Info (Lme/proton/core/auth/domain/entity/Fido2Info;)V
|
||||
public final fun unlock (Lme/proton/core/domain/entity/UserId;Lme/proton/core/network/domain/scopes/Scope;Ljava/lang/String;Ljava/lang/String;)Lkotlinx/coroutines/Job;
|
||||
public final fun unlock (Lme/proton/core/domain/entity/UserId;Lme/proton/core/network/domain/scopes/Scope;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/user/domain/entity/SecondFactorFido;)Lkotlinx/coroutines/Job;
|
||||
public static synthetic fun unlock$default (Lme/proton/core/auth/presentation/viewmodel/ConfirmPasswordDialogViewModel;Lme/proton/core/domain/entity/UserId;Lme/proton/core/network/domain/scopes/Scope;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/user/domain/entity/SecondFactorFido;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
|
||||
}
|
||||
|
||||
public abstract class me/proton/core/auth/presentation/viewmodel/ConfirmPasswordDialogViewModel$State {
|
||||
|
||||
+97
-14
@@ -22,6 +22,7 @@ import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
@@ -32,7 +33,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.auth.domain.entity.SecondFactorMethod
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2PublicKeyCredentialRequestOptions
|
||||
import me.proton.core.auth.fido.domain.usecase.PerformTwoFaWithSecurityKey
|
||||
import me.proton.core.auth.presentation.LogTag
|
||||
import me.proton.core.auth.presentation.R
|
||||
import me.proton.core.auth.presentation.databinding.DialogConfirmPasswordBinding
|
||||
import me.proton.core.auth.presentation.entity.confirmpass.ConfirmPasswordInput
|
||||
@@ -43,14 +48,22 @@ import me.proton.core.network.domain.scopes.MissingScopeState
|
||||
import me.proton.core.network.domain.scopes.Scope
|
||||
import me.proton.core.presentation.utils.ProtectScreenConfiguration
|
||||
import me.proton.core.presentation.utils.ScreenContentProtector
|
||||
import me.proton.core.presentation.utils.errorSnack
|
||||
import me.proton.core.presentation.utils.errorToast
|
||||
import me.proton.core.presentation.utils.openBrowserLink
|
||||
import me.proton.core.user.domain.entity.SecondFactorFido
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
/**
|
||||
* This dialog handles only [Scope.PASSWORD] or [Scope.LOCKED]. Any other scope will be ignored.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ConfirmPasswordDialog : DialogFragment() {
|
||||
@Inject
|
||||
lateinit var performTwoFaWithSecurityKey: Optional<PerformTwoFaWithSecurityKey<ComponentActivity>>
|
||||
|
||||
private val viewModel by viewModels<ConfirmPasswordDialogViewModel>()
|
||||
|
||||
@@ -83,16 +96,8 @@ class ConfirmPasswordDialog : DialogFragment() {
|
||||
ConfirmPasswordDialogViewController(
|
||||
DialogConfirmPasswordBinding.inflate(layoutInflater),
|
||||
lifecycleOwner = this,
|
||||
onEnterButtonClick = { selectedSecondFactorMethod ->
|
||||
when (selectedSecondFactorMethod) {
|
||||
SecondFactorMethod.Totp -> onTotpSubmitted()
|
||||
SecondFactorMethod.Authenticator -> onSecurityKeySubmitted()
|
||||
null -> Unit
|
||||
}
|
||||
},
|
||||
onCancelButtonClick = {
|
||||
setResultAndDismiss(null)
|
||||
},
|
||||
onEnterButtonClick = this::onEnterButtonClick,
|
||||
onCancelButtonClick = { setResultAndDismiss(null) },
|
||||
onSecurityKeyInfoClick = {
|
||||
context?.let {
|
||||
it.openBrowserLink(it.getString(R.string.confirm_password_2fa_security_key))
|
||||
@@ -101,6 +106,11 @@ class ConfirmPasswordDialog : DialogFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
performTwoFaWithSecurityKey.getOrNull()?.register(requireActivity(), this::onTwoFaWithSecurityKeyResult)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
super.onCreateDialog(savedInstanceState)
|
||||
screenProtector.protect(requireActivity())
|
||||
@@ -129,15 +139,45 @@ class ConfirmPasswordDialog : DialogFragment() {
|
||||
.setView(viewController.root)
|
||||
.create()
|
||||
|
||||
private fun onEnterButtonClick(selectedSecondFactorMethod: SecondFactorMethod?) {
|
||||
val password = viewController.password.orEmpty()
|
||||
when (missingScope) {
|
||||
Scope.PASSWORD -> when (selectedSecondFactorMethod) {
|
||||
SecondFactorMethod.Totp -> onTotpSubmitted()
|
||||
SecondFactorMethod.Authenticator -> onSecurityKeySubmitted()
|
||||
null -> viewModel.unlock(userId, missingScope, password)
|
||||
}
|
||||
|
||||
Scope.LOCKED -> viewModel.unlock(userId, missingScope, password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTotpSubmitted() {
|
||||
val password = viewController.password
|
||||
val password = viewController.password.orEmpty()
|
||||
val twoFactorCode = viewController.twoFactorCode
|
||||
viewModel.unlock(userId, missingScope, password.orEmpty(), twoFactorCode)
|
||||
viewModel.unlock(userId, missingScope, password, secondFactorCode = twoFactorCode)
|
||||
}
|
||||
|
||||
private fun onSecurityKeySubmitted() {
|
||||
// TODO Launch Fido2ApiClient (with data from viewModel.fido2Info),
|
||||
// get the result and pass it back to the view model for final processing.
|
||||
val performTwoFaWithSecurityKey = performTwoFaWithSecurityKey.getOrNull() ?: return
|
||||
val requestOptions = requireNotNull(viewModel.fido2Info?.authenticationOptions?.publicKey)
|
||||
|
||||
viewController.setLoading()
|
||||
|
||||
lifecycleScope.launch {
|
||||
val activity = activity ?: return@launch
|
||||
when (val launchResult = performTwoFaWithSecurityKey.invoke(activity, requestOptions)) {
|
||||
is PerformTwoFaWithSecurityKey.LaunchResult.Failure -> {
|
||||
viewController.setIdle()
|
||||
viewController.root.errorSnack(
|
||||
message = launchResult.exception.localizedMessage
|
||||
?: getString(R.string.auth_login_general_error)
|
||||
)
|
||||
}
|
||||
|
||||
is PerformTwoFaWithSecurityKey.LaunchResult.Success -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleState(state: ConfirmPasswordDialogViewModel.State) = when (state) {
|
||||
@@ -177,6 +217,49 @@ class ConfirmPasswordDialog : DialogFragment() {
|
||||
screenProtector.unprotect(requireActivity())
|
||||
}
|
||||
|
||||
private fun onTwoFaWithSecurityKeyResult(
|
||||
result: PerformTwoFaWithSecurityKey.Result,
|
||||
options: Fido2PublicKeyCredentialRequestOptions
|
||||
) {
|
||||
viewController.setIdle()
|
||||
|
||||
when (result) {
|
||||
is PerformTwoFaWithSecurityKey.Result.Success -> onSecurityKeyAuthSuccess(result, options)
|
||||
is PerformTwoFaWithSecurityKey.Result.Cancelled -> Unit
|
||||
is PerformTwoFaWithSecurityKey.Result.EmptyResult -> viewController.root.errorSnack(
|
||||
getString(R.string.auth_login_general_error)
|
||||
)
|
||||
|
||||
is PerformTwoFaWithSecurityKey.Result.Error -> viewController.root.errorSnack(
|
||||
result.error.message ?: getString(R.string.auth_login_general_error)
|
||||
)
|
||||
|
||||
is PerformTwoFaWithSecurityKey.Result.UnknownResult -> {
|
||||
getString(R.string.auth_login_general_error)
|
||||
CoreLogger.e(LogTag.FLOW_ERROR_2FA, result.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSecurityKeyAuthSuccess(
|
||||
result: PerformTwoFaWithSecurityKey.Result.Success,
|
||||
options: Fido2PublicKeyCredentialRequestOptions
|
||||
) {
|
||||
val password = viewController.password.orEmpty()
|
||||
viewModel.unlock(
|
||||
userId = userId,
|
||||
missingScope = missingScope,
|
||||
password = password,
|
||||
secondFactorFido = SecondFactorFido(
|
||||
publicKeyOptions = options,
|
||||
clientData = result.response.clientDataJSON,
|
||||
authenticatorData = result.response.authenticatorData,
|
||||
signature = result.response.signature,
|
||||
credentialID = result.rawId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_INPUT = "arg.confirmPasswordInput"
|
||||
|
||||
|
||||
+5
-2
@@ -44,6 +44,7 @@ import me.proton.core.network.domain.scopes.MissingScopeListener
|
||||
import me.proton.core.network.domain.scopes.MissingScopeState
|
||||
import me.proton.core.network.domain.scopes.Scope
|
||||
import me.proton.core.presentation.viewmodel.ProtonViewModel
|
||||
import me.proton.core.user.domain.entity.SecondFactorFido
|
||||
import me.proton.core.util.kotlin.exhaustive
|
||||
import me.proton.core.util.kotlin.takeIfNotEmpty
|
||||
import javax.inject.Inject
|
||||
@@ -107,7 +108,8 @@ class ConfirmPasswordDialogViewModel @Inject constructor(
|
||||
userId: UserId,
|
||||
missingScope: Scope,
|
||||
password: String,
|
||||
twoFactorCode: String?
|
||||
secondFactorCode: String? = null,
|
||||
secondFactorFido: SecondFactorFido? = null
|
||||
) = flow {
|
||||
emit(State.ProcessingObtainScope)
|
||||
val account = accountManager.getAccount(userId).firstOrNull()
|
||||
@@ -121,7 +123,8 @@ class ConfirmPasswordDialogViewModel @Inject constructor(
|
||||
sessionId = requireNotNull(account.sessionId),
|
||||
username = requireNotNull(account.username),
|
||||
password = password.encrypt(keyStoreCrypto),
|
||||
twoFactorCode = twoFactorCode?.takeIfNotEmpty()
|
||||
secondFactorCode = secondFactorCode?.takeIfNotEmpty(),
|
||||
secondFactorFido = secondFactorFido
|
||||
)
|
||||
|
||||
Scope.LOCKED -> obtainLockedScope(
|
||||
|
||||
+6
-2
@@ -195,6 +195,7 @@ class ConfirmPasswordDialogViewModelTest :
|
||||
testSessionId,
|
||||
testUsername,
|
||||
testPasswordEncrypted,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} returns true
|
||||
@@ -220,6 +221,7 @@ class ConfirmPasswordDialogViewModelTest :
|
||||
testSessionId,
|
||||
testUsername,
|
||||
testPasswordEncrypted,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} throws ApiException(
|
||||
@@ -253,7 +255,8 @@ class ConfirmPasswordDialogViewModelTest :
|
||||
testSessionId,
|
||||
testUsername,
|
||||
testPasswordEncrypted,
|
||||
test2FACode
|
||||
test2FACode,
|
||||
null
|
||||
)
|
||||
} returns true
|
||||
flowTest(viewModel.state) {
|
||||
@@ -278,7 +281,8 @@ class ConfirmPasswordDialogViewModelTest :
|
||||
testSessionId,
|
||||
testUsername,
|
||||
testPasswordEncrypted,
|
||||
test2FACode
|
||||
test2FACode,
|
||||
null
|
||||
)
|
||||
} throws ApiException(
|
||||
ApiResult.Error.Http(
|
||||
|
||||
@@ -426,17 +426,19 @@ public final class me/proton/core/user/data/api/request/CreateUserRequest$Compan
|
||||
|
||||
public final class me/proton/core/user/data/api/request/UnlockPasswordRequest {
|
||||
public static final field Companion Lme/proton/core/user/data/api/request/UnlockPasswordRequest$Companion;
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
|
||||
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/auth/data/api/request/Fido2Request;)V
|
||||
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/auth/data/api/request/Fido2Request;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Ljava/lang/String;
|
||||
public final fun component2 ()Ljava/lang/String;
|
||||
public final fun component3 ()Ljava/lang/String;
|
||||
public final fun component4 ()Ljava/lang/String;
|
||||
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/user/data/api/request/UnlockPasswordRequest;
|
||||
public static synthetic fun copy$default (Lme/proton/core/user/data/api/request/UnlockPasswordRequest;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/user/data/api/request/UnlockPasswordRequest;
|
||||
public final fun component5 ()Lme/proton/core/auth/data/api/request/Fido2Request;
|
||||
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/auth/data/api/request/Fido2Request;)Lme/proton/core/user/data/api/request/UnlockPasswordRequest;
|
||||
public static synthetic fun copy$default (Lme/proton/core/user/data/api/request/UnlockPasswordRequest;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/auth/data/api/request/Fido2Request;ILjava/lang/Object;)Lme/proton/core/user/data/api/request/UnlockPasswordRequest;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getClientEphemeral ()Ljava/lang/String;
|
||||
public final fun getClientProof ()Ljava/lang/String;
|
||||
public final fun getFido2 ()Lme/proton/core/auth/data/api/request/Fido2Request;
|
||||
public final fun getSrpSession ()Ljava/lang/String;
|
||||
public final fun getTwoFactorCode ()Ljava/lang/String;
|
||||
public fun hashCode ()I
|
||||
@@ -1000,7 +1002,7 @@ public final class me/proton/core/user/data/repository/UserRepositoryImpl : me/p
|
||||
public fun removeLockedAndPasswordScopes (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun setPassphrase (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/keystore/EncryptedByteArray;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun unlockUserForLockedScope (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/srp/SrpProofs;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun unlockUserForPasswordScope (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/srp/SrpProofs;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun unlockUserForPasswordScope (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/srp/SrpProofs;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/user/domain/entity/SecondFactorFido;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun updateUser (Lme/proton/core/user/domain/entity/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun updateUserUsedBaseSpace (Lme/proton/core/domain/entity/UserId;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public fun updateUserUsedDriveSpace (Lme/proton/core/domain/entity/UserId;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
|
||||
@@ -30,8 +30,8 @@ protonBuild {
|
||||
}
|
||||
|
||||
protonCoverage {
|
||||
branchCoveragePercentage.set(34)
|
||||
lineCoveragePercentage.set(37)
|
||||
branchCoveragePercentage.set(33)
|
||||
lineCoveragePercentage.set(36)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
+75
-2
@@ -25,6 +25,7 @@ import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
@@ -35,9 +36,15 @@ import me.proton.core.account.data.repository.AccountRepositoryImpl
|
||||
import me.proton.core.accountmanager.data.AccountManagerImpl
|
||||
import me.proton.core.accountmanager.data.db.AccountManagerDatabase
|
||||
import me.proton.core.accountmanager.domain.AccountManager
|
||||
import me.proton.core.auth.data.api.fido2.AuthenticationOptionsData
|
||||
import me.proton.core.auth.data.api.fido2.PublicKeyCredentialDescriptorData
|
||||
import me.proton.core.auth.data.api.fido2.PublicKeyCredentialRequestOptionsResponse
|
||||
import me.proton.core.auth.data.api.request.Fido2Request
|
||||
import me.proton.core.auth.data.api.response.SRPAuthenticationResponse
|
||||
import me.proton.core.auth.domain.exception.InvalidServerAuthenticationException
|
||||
import me.proton.core.auth.domain.usecase.ValidateServerProof
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2PublicKeyCredentialDescriptor
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2PublicKeyCredentialRequestOptions
|
||||
import me.proton.core.challenge.domain.entity.ChallengeFrameDetails
|
||||
import me.proton.core.crypto.android.context.AndroidCryptoContext
|
||||
import me.proton.core.crypto.common.context.CryptoContext
|
||||
@@ -68,6 +75,7 @@ import me.proton.core.user.data.api.request.UnlockPasswordRequest
|
||||
import me.proton.core.user.data.entity.UserEntity
|
||||
import me.proton.core.user.data.extension.toUser
|
||||
import me.proton.core.user.domain.entity.CreateUserType
|
||||
import me.proton.core.user.domain.entity.SecondFactorFido
|
||||
import me.proton.core.user.domain.entity.Type
|
||||
import me.proton.core.user.domain.repository.UserLocalDataSource
|
||||
import me.proton.core.user.domain.repository.UserRemoteDataSource
|
||||
@@ -75,6 +83,7 @@ import me.proton.core.user.domain.repository.UserRepository
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
@@ -90,7 +99,7 @@ class UserRepositoryImplTests {
|
||||
@MockK(relaxed = true)
|
||||
private lateinit var apiManagerFactory: ApiManagerFactory
|
||||
|
||||
@MockK(relaxed = true)
|
||||
@MockK
|
||||
private lateinit var userApi: UserApi
|
||||
|
||||
private val cryptoContext: CryptoContext = AndroidCryptoContext(
|
||||
@@ -477,6 +486,7 @@ class UserRepositoryImplTests {
|
||||
TestUsers.User1.id,
|
||||
testSrpProofs,
|
||||
"test-srp-session",
|
||||
null,
|
||||
null
|
||||
)
|
||||
assertNotNull(response)
|
||||
@@ -507,12 +517,75 @@ class UserRepositoryImplTests {
|
||||
TestUsers.User1.id,
|
||||
testSrpProofs,
|
||||
"test-srp-session",
|
||||
"test-2fa"
|
||||
"test-2fa",
|
||||
null
|
||||
)
|
||||
assertNotNull(response)
|
||||
assertTrue(response)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
@Test
|
||||
fun unlockUser_2fa_fido_passwordScope() = runTest {
|
||||
// GIVEN
|
||||
val credentialId = ubyteArrayOf(7U, 8U)
|
||||
val challenge = ubyteArrayOf(9U, 10U)
|
||||
val credentialType = "credential-type"
|
||||
val unlockPasswordRequestSlot = slot<UnlockPasswordRequest>()
|
||||
|
||||
coEvery {
|
||||
userApi.unlockPasswordScope(capture(unlockPasswordRequestSlot))
|
||||
} answers {
|
||||
SRPAuthenticationResponse(
|
||||
code = 1000,
|
||||
serverProof = testSrpProofs.expectedServerProof,
|
||||
)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
val response = userRepository.unlockUserForPasswordScope(
|
||||
TestUsers.User1.id,
|
||||
testSrpProofs,
|
||||
"test-srp-session",
|
||||
null,
|
||||
SecondFactorFido(
|
||||
publicKeyOptions = Fido2PublicKeyCredentialRequestOptions(
|
||||
challenge = challenge,
|
||||
allowCredentials = listOf(
|
||||
Fido2PublicKeyCredentialDescriptor(
|
||||
type = credentialType,
|
||||
id = credentialId,
|
||||
transports = null
|
||||
)
|
||||
)
|
||||
),
|
||||
clientData = byteArrayOf(1, 2),
|
||||
authenticatorData = byteArrayOf(3, 4),
|
||||
signature = byteArrayOf(5, 6),
|
||||
credentialID = credentialId.toByteArray()
|
||||
)
|
||||
)
|
||||
assertNotNull(response)
|
||||
assertTrue(response)
|
||||
|
||||
val capturedFido2Request = unlockPasswordRequestSlot.captured.fido2!!
|
||||
val capturedPublicKey = capturedFido2Request.authenticationOptions.publicKey
|
||||
assertEquals("AQI=", capturedFido2Request.clientData)
|
||||
assertEquals("AwQ=", capturedFido2Request.authenticatorData)
|
||||
assertEquals("BQY=", capturedFido2Request.signature)
|
||||
assertContentEquals(credentialId, capturedFido2Request.credentialID)
|
||||
assertContentEquals(challenge, capturedPublicKey.challenge)
|
||||
assertContentEquals(
|
||||
listOf(
|
||||
PublicKeyCredentialDescriptorData(
|
||||
type = credentialType,
|
||||
id = credentialId,
|
||||
transports = null
|
||||
)
|
||||
), capturedPublicKey.allowCredentials
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unlockUser_wrong_server_proof(): Unit = runTest {
|
||||
// GIVEN
|
||||
|
||||
+4
-1
@@ -20,6 +20,7 @@ package me.proton.core.user.data.api.request
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.proton.core.auth.data.api.request.Fido2Request
|
||||
|
||||
@Serializable
|
||||
data class UnlockPasswordRequest(
|
||||
@@ -30,5 +31,7 @@ data class UnlockPasswordRequest(
|
||||
@SerialName("SRPSession")
|
||||
val srpSession: String,
|
||||
@SerialName("TwoFactorCode")
|
||||
val twoFactorCode: String? = null
|
||||
val twoFactorCode: String? = null,
|
||||
@SerialName("FIDO2")
|
||||
val fido2: Fido2Request? = null
|
||||
)
|
||||
|
||||
+40
-2
@@ -19,6 +19,7 @@
|
||||
package me.proton.core.user.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import com.dropbox.android.external.store4.Fetcher
|
||||
import com.dropbox.android.external.store4.SourceOfTruth
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
@@ -27,8 +28,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.proton.core.auth.data.api.fido2.AuthenticationOptionsData
|
||||
import me.proton.core.auth.data.api.fido2.PublicKeyCredentialDescriptorData
|
||||
import me.proton.core.auth.data.api.fido2.PublicKeyCredentialRequestOptionsResponse
|
||||
import me.proton.core.auth.data.api.request.Fido2Request
|
||||
import me.proton.core.auth.data.api.response.isSuccess
|
||||
import me.proton.core.auth.domain.usecase.ValidateServerProof
|
||||
import me.proton.core.auth.fido.domain.ext.toJson
|
||||
import me.proton.core.challenge.data.frame.ChallengeFrame
|
||||
import me.proton.core.challenge.domain.entity.ChallengeFrameDetails
|
||||
import me.proton.core.challenge.domain.framePrefix
|
||||
@@ -50,6 +56,7 @@ import me.proton.core.user.data.api.request.UnlockPasswordRequest
|
||||
import me.proton.core.user.data.api.request.UnlockRequest
|
||||
import me.proton.core.user.data.extension.toUser
|
||||
import me.proton.core.user.domain.entity.CreateUserType
|
||||
import me.proton.core.user.domain.entity.SecondFactorFido
|
||||
import me.proton.core.user.domain.entity.User
|
||||
import me.proton.core.user.domain.repository.PassphraseRepository
|
||||
import me.proton.core.user.domain.repository.UserLocalDataSource
|
||||
@@ -198,14 +205,16 @@ class UserRepositoryImpl @Inject constructor(
|
||||
sessionUserId: SessionUserId,
|
||||
srpProofs: SrpProofs,
|
||||
srpSession: String,
|
||||
twoFactorCode: String?
|
||||
secondFactorCode: String?,
|
||||
secondFactorFido: SecondFactorFido?
|
||||
): Boolean =
|
||||
provider.get<UserApi>(sessionUserId).invoke {
|
||||
val request = UnlockPasswordRequest(
|
||||
srpProofs.clientEphemeral,
|
||||
srpProofs.clientProof,
|
||||
srpSession,
|
||||
twoFactorCode
|
||||
secondFactorCode,
|
||||
secondFactorFido?.toFido2Request()
|
||||
)
|
||||
val response = unlockPasswordScope(request)
|
||||
validateServerProof(
|
||||
@@ -276,3 +285,32 @@ class UserRepositoryImpl @Inject constructor(
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
private fun SecondFactorFido.toFido2Request(): Fido2Request {
|
||||
val optionsData = AuthenticationOptionsData(
|
||||
PublicKeyCredentialRequestOptionsResponse(
|
||||
challenge = publicKeyOptions.challenge,
|
||||
timeout = publicKeyOptions.timeout,
|
||||
rpId = publicKeyOptions.rpId,
|
||||
allowCredentials = publicKeyOptions.allowCredentials?.map {
|
||||
PublicKeyCredentialDescriptorData(
|
||||
type = it.type,
|
||||
id = it.id,
|
||||
transports = it.transports
|
||||
)
|
||||
},
|
||||
userVerification = publicKeyOptions.userVerification,
|
||||
extensions = publicKeyOptions.extensions?.toJson()
|
||||
)
|
||||
)
|
||||
return Fido2Request(
|
||||
authenticationOptions = optionsData,
|
||||
clientData = clientData.toBase64(),
|
||||
authenticatorData = authenticatorData.toBase64(),
|
||||
signature = signature.toBase64(),
|
||||
credentialID = credentialID.toUByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
private fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP)
|
||||
|
||||
@@ -205,6 +205,15 @@ public final class me/proton/core/user/domain/entity/Role$Companion {
|
||||
public final fun getMap ()Ljava/util/Map;
|
||||
}
|
||||
|
||||
public final class me/proton/core/user/domain/entity/SecondFactorFido {
|
||||
public fun <init> (Lme/proton/core/auth/fido/domain/entity/Fido2PublicKeyCredentialRequestOptions;[B[B[B[B)V
|
||||
public final fun getAuthenticatorData ()[B
|
||||
public final fun getClientData ()[B
|
||||
public final fun getCredentialID ()[B
|
||||
public final fun getPublicKeyOptions ()Lme/proton/core/auth/fido/domain/entity/Fido2PublicKeyCredentialRequestOptions;
|
||||
public final fun getSignature ()[B
|
||||
}
|
||||
|
||||
public final class me/proton/core/user/domain/entity/Type : java/lang/Enum {
|
||||
public static final field Companion Lme/proton/core/user/domain/entity/Type$Companion;
|
||||
public static final field CredentialLess Lme/proton/core/user/domain/entity/Type;
|
||||
@@ -551,7 +560,7 @@ public abstract interface class me/proton/core/user/domain/repository/UserReposi
|
||||
public abstract fun observeUser (Lme/proton/core/domain/entity/UserId;Z)Lkotlinx/coroutines/flow/Flow;
|
||||
public abstract fun removeLockedAndPasswordScopes (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun unlockUserForLockedScope (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/srp/SrpProofs;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun unlockUserForPasswordScope (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/srp/SrpProofs;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun unlockUserForPasswordScope (Lme/proton/core/domain/entity/UserId;Lme/proton/core/crypto/common/srp/SrpProofs;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/user/domain/entity/SecondFactorFido;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun updateUser (Lme/proton/core/user/domain/entity/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun updateUserUsedBaseSpace (Lme/proton/core/domain/entity/UserId;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun updateUserUsedDriveSpace (Lme/proton/core/domain/entity/UserId;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
|
||||
@@ -30,12 +30,13 @@ publishOption.shouldBePublishedAsLib = true
|
||||
|
||||
protonCoverage {
|
||||
branchCoveragePercentage.set(77)
|
||||
lineCoveragePercentage.set(56)
|
||||
lineCoveragePercentage.set(55)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(
|
||||
project(Module.accountDomain),
|
||||
project(Module.authFidoDomain),
|
||||
project(Module.challengeDomain),
|
||||
project(Module.cryptoCommon),
|
||||
project(Module.domain),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.user.domain.entity
|
||||
|
||||
import me.proton.core.auth.fido.domain.entity.Fido2PublicKeyCredentialRequestOptions
|
||||
|
||||
class SecondFactorFido(
|
||||
val publicKeyOptions: Fido2PublicKeyCredentialRequestOptions,
|
||||
val clientData: ByteArray,
|
||||
val authenticatorData: ByteArray,
|
||||
val signature: ByteArray,
|
||||
val credentialID: ByteArray
|
||||
)
|
||||
@@ -29,6 +29,7 @@ import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.network.domain.session.SessionId
|
||||
import me.proton.core.user.domain.entity.CreateUserType
|
||||
import me.proton.core.user.domain.entity.Domain
|
||||
import me.proton.core.user.domain.entity.SecondFactorFido
|
||||
import me.proton.core.user.domain.entity.User
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@@ -154,7 +155,8 @@ interface UserRepository : PassphraseRepository {
|
||||
sessionUserId: SessionUserId,
|
||||
srpProofs: SrpProofs,
|
||||
srpSession: String,
|
||||
twoFactorCode: String?
|
||||
secondFactorCode: String?,
|
||||
secondFactorFido: SecondFactorFido?
|
||||
): Boolean
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user