mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 02:15:53 +03:00
[PM-10902] Base64 encode sensitive 2FA nav args (#3841)
This commit is contained in:
parent
4c983525d3
commit
f778d7ecd1
8 changed files with 113 additions and 44 deletions
|
@ -110,7 +110,7 @@ fun NavGraphBuilder.authGraph(
|
|||
onNavigateToTwoFactorLogin = { emailAddress ->
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = emailAddress,
|
||||
password = null,
|
||||
base64EncodedPassword = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -153,7 +153,7 @@ fun NavGraphBuilder.authGraph(
|
|||
onNavigateToTwoFactorLogin = { emailAddress, password ->
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = emailAddress,
|
||||
password = password,
|
||||
base64EncodedPassword = password,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -162,7 +162,7 @@ fun NavGraphBuilder.authGraph(
|
|||
onNavigateToTwoFactorLogin = {
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = it,
|
||||
password = null,
|
||||
base64EncodedPassword = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -94,7 +94,7 @@ fun LoginScreen(
|
|||
}
|
||||
|
||||
is LoginEvent.NavigateToTwoFactorLogin -> {
|
||||
onNavigateToTwoFactorLogin(event.emailAddress, event.password)
|
||||
onNavigateToTwoFactorLogin(event.emailAddress, event.base64EncodedPassword)
|
||||
}
|
||||
|
||||
is LoginEvent.ShowToast -> {
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -159,7 +160,9 @@ class LoginViewModel @Inject constructor(
|
|||
sendEvent(
|
||||
LoginEvent.NavigateToTwoFactorLogin(
|
||||
emailAddress = state.emailAddress,
|
||||
password = state.passwordInput,
|
||||
// Base64 URL encode the password to prevent corruption of escapable chars
|
||||
// when sending via navArgs.
|
||||
base64EncodedPassword = state.passwordInput.base64UrlEncode(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -342,7 +345,7 @@ sealed class LoginEvent {
|
|||
*/
|
||||
data class NavigateToTwoFactorLogin(
|
||||
val emailAddress: String,
|
||||
val password: String?,
|
||||
val base64EncodedPassword: String?,
|
||||
) : LoginEvent()
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,7 +28,7 @@ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) {
|
|||
onNavigateToTwoFactorLogin = {
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = it,
|
||||
password = null,
|
||||
base64EncodedPassword = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -15,12 +15,14 @@ private const val TWO_FACTOR_LOGIN_ROUTE =
|
|||
|
||||
/**
|
||||
* Class to retrieve Two-Factor Login arguments from the [SavedStateHandle].
|
||||
*
|
||||
* @property base64EncodedPassword Base64 URL encoded password input.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class TwoFactorLoginArgs(val emailAddress: String, val password: String?) {
|
||||
data class TwoFactorLoginArgs(val emailAddress: String, val base64EncodedPassword: String?) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
emailAddress = checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||
password = savedStateHandle[PASSWORD],
|
||||
base64EncodedPassword = savedStateHandle[PASSWORD],
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -29,10 +31,13 @@ data class TwoFactorLoginArgs(val emailAddress: String, val password: String?) {
|
|||
*/
|
||||
fun NavController.navigateToTwoFactorLogin(
|
||||
emailAddress: String,
|
||||
password: String?,
|
||||
base64EncodedPassword: String?,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("$TWO_FACTOR_LOGIN_PREFIX/$emailAddress?$PASSWORD=$password", navOptions)
|
||||
this.navigate(
|
||||
route = "$TWO_FACTOR_LOGIN_PREFIX/$emailAddress?$PASSWORD=$base64EncodedPassword",
|
||||
navOptions = navOptions,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.button
|
||||
|
@ -56,7 +57,9 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<TwoFactorLoginState, TwoFactorLoginEvent, TwoFactorLoginAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: TwoFactorLoginState(
|
||||
?: run {
|
||||
val args = TwoFactorLoginArgs(savedStateHandle)
|
||||
TwoFactorLoginState(
|
||||
authMethod = authRepository.twoFactorResponse.preferredAuthMethod,
|
||||
availableAuthMethods = authRepository.twoFactorResponse.availableAuthMethods,
|
||||
codeInput = "",
|
||||
|
@ -68,9 +71,10 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
.isContinueButtonEnabled,
|
||||
isRememberMeEnabled = false,
|
||||
captchaToken = null,
|
||||
email = TwoFactorLoginArgs(savedStateHandle).emailAddress,
|
||||
password = TwoFactorLoginArgs(savedStateHandle).password,
|
||||
),
|
||||
email = args.emailAddress,
|
||||
password = args.base64EncodedPassword?.base64UrlDecodeOrNull(),
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
private val recover2faUri: Uri
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
|
@ -60,7 +61,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(::generateUriForCaptcha)
|
||||
unmockkStatic(::generateUriForCaptcha, String::base64UrlEncode)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -333,6 +334,51 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LoginButtonClick login returns TwoFactorRequired should base64 URL encode password and emit NavigateToTwoFactorLogin`() =
|
||||
runTest {
|
||||
mockkStatic(String::base64UrlEncode)
|
||||
val decodedPassword = "password"
|
||||
val encodedPassword = "base64EncodedPassword"
|
||||
every { decodedPassword.base64UrlEncode() } returns encodedPassword
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = decodedPassword,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.TwoFactorRequired
|
||||
|
||||
val viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
passwordInput = decodedPassword,
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LoginAction.LoginButtonClick)
|
||||
verify { decodedPassword.base64UrlEncode() }
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(passwordInput = decodedPassword),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
LoginEvent.NavigateToTwoFactorLogin(
|
||||
EMAIL,
|
||||
encodedPassword,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = decodedPassword,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MasterPasswordHintClick should emit NavigateToMasterPasswordHint`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
|
|
@ -17,6 +17,8 @@ import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
|
||||
|
@ -64,8 +66,12 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
mockkStatic(
|
||||
::generateUriForCaptcha,
|
||||
::generateUriForWebAuth,
|
||||
String::base64UrlEncode,
|
||||
)
|
||||
mockkStatic(Uri::class)
|
||||
every {
|
||||
DEFAULT_ENCODED_PASSWORD.base64UrlDecodeOrNull()
|
||||
} returns DEFAULT_PASSWORD
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
@ -73,6 +79,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
unmockkStatic(
|
||||
::generateUriForCaptcha,
|
||||
::generateUriForWebAuth,
|
||||
String::base64UrlDecodeOrNull,
|
||||
)
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
@ -81,6 +88,9 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
verify {
|
||||
DEFAULT_ENCODED_PASSWORD.base64UrlDecodeOrNull()
|
||||
}
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +117,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = token,
|
||||
method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
|
||||
|
@ -127,7 +137,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify(exactly = 1) {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = token,
|
||||
method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
|
||||
|
@ -158,7 +168,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -172,7 +182,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -188,7 +198,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "token",
|
||||
method = TwoFactorAuthMethod.DUO.value.toString(),
|
||||
|
@ -206,7 +216,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "token",
|
||||
method = TwoFactorAuthMethod.DUO.value.toString(),
|
||||
|
@ -274,7 +284,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -305,7 +315,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -460,7 +470,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -486,7 +496,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -502,7 +512,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -542,7 +552,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -559,7 +569,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -599,7 +609,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
|
@ -840,7 +850,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
savedStateHandle = SavedStateHandle().also {
|
||||
it["state"] = state
|
||||
it["email_address"] = "example@email.com"
|
||||
it["password"] = "password123"
|
||||
it["password"] = DEFAULT_ENCODED_PASSWORD
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -858,7 +868,8 @@ private val TWO_FACTOR_RESPONSE = GetTokenResponseJson.TwoFactorRequired(
|
|||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
|
||||
private const val DEFAULT_PASSWORD = "password123"
|
||||
private const val DEFAULT_ENCODED_PASSWORD = "base64EncodedPassword"
|
||||
private val DEFAULT_STATE = TwoFactorLoginState(
|
||||
authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP,
|
||||
availableAuthMethods = listOf(
|
||||
|
@ -873,5 +884,5 @@ private val DEFAULT_STATE = TwoFactorLoginState(
|
|||
isRememberMeEnabled = false,
|
||||
captchaToken = null,
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
password = DEFAULT_PASSWORD,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue