diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt index ab5a55076..210f7fd6f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt @@ -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, ) }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt index 67fc315b6..212d4abd1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt @@ -94,7 +94,7 @@ fun LoginScreen( } is LoginEvent.NavigateToTwoFactorLogin -> { - onNavigateToTwoFactorLogin(event.emailAddress, event.password) + onNavigateToTwoFactorLogin(event.emailAddress, event.base64EncodedPassword) } is LoginEvent.ShowToast -> { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt index 8218ce66f..7deb3f939 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt @@ -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() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt index 2a77ebd98..a4ef76dc8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt @@ -28,7 +28,7 @@ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) { onNavigateToTwoFactorLogin = { navController.navigateToTwoFactorLogin( emailAddress = it, - password = null, + base64EncodedPassword = null, ) }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt index 3b92650b3..12f39f717 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt @@ -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, + ) } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt index c089fdcf8..eec1c672a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt @@ -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,21 +57,24 @@ class TwoFactorLoginViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] - ?: TwoFactorLoginState( - authMethod = authRepository.twoFactorResponse.preferredAuthMethod, - availableAuthMethods = authRepository.twoFactorResponse.availableAuthMethods, - codeInput = "", - displayEmail = authRepository.twoFactorResponse.twoFactorDisplayEmail, - dialogState = null, - isContinueButtonEnabled = authRepository - .twoFactorResponse - .preferredAuthMethod - .isContinueButtonEnabled, - isRememberMeEnabled = false, - captchaToken = null, - email = TwoFactorLoginArgs(savedStateHandle).emailAddress, - password = TwoFactorLoginArgs(savedStateHandle).password, - ), + ?: run { + val args = TwoFactorLoginArgs(savedStateHandle) + TwoFactorLoginState( + authMethod = authRepository.twoFactorResponse.preferredAuthMethod, + availableAuthMethods = authRepository.twoFactorResponse.availableAuthMethods, + codeInput = "", + displayEmail = authRepository.twoFactorResponse.twoFactorDisplayEmail, + dialogState = null, + isContinueButtonEnabled = authRepository + .twoFactorResponse + .preferredAuthMethod + .isContinueButtonEnabled, + isRememberMeEnabled = false, + captchaToken = null, + email = args.emailAddress, + password = args.base64EncodedPassword?.base64UrlDecodeOrNull(), + ) + }, ) { private val recover2faUri: Uri diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index e71e218a2..c284aff3c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -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() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt index 7daa7dfa5..48feee6fd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt @@ -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, )