Skip to content

Commit b459632

Browse files
authored
Merge branch 'main' into pm-8217/new-device-notice-ui
2 parents cb28215 + e2b93ec commit b459632

File tree

20 files changed

+468
-11
lines changed

20 files changed

+468
-11
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
66

77
# Default file owners.
8-
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront
8+
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront @phil-livefront
99

1010
# Actions and workflow changes.
1111
.github/ @bitwarden/dept-development-mobile

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ jobs:
460460
distribution: "temurin"
461461
java-version: ${{ env.JAVA_VERSION }}
462462

463+
- name: Update app CI Build info
464+
run: |
465+
./Scripts/update_app_ci_build_info.sh $GITHUB_REPOSITORY $GITHUB_REF_NAME $GITHUB_SHA $GITHUB_RUN_ID $GITHUB_RUN_ATTEMPT
466+
463467
# Start from 11000 to prevent collisions with mobile build version codes
464468
- name: Increment version
465469
run: |

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ android {
6868
buildConfigField(
6969
type = "String",
7070
name = "CI_INFO",
71-
value = "\"${ciProperties.getOrDefault("ci.info", "local")}\""
71+
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
7272
)
7373
}
7474

app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
9494
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
9595
import com.x8bit.bitwarden.data.auth.util.toSdkParams
9696
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
97+
import com.x8bit.bitwarden.data.platform.datasource.network.util.isSslHandShakeError
9798
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
9899
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
99100
import com.x8bit.bitwarden.data.platform.manager.LogsManager
@@ -624,7 +625,12 @@ class AuthRepositoryImpl(
624625
)
625626
}
626627
.fold(
627-
onFailure = { LoginResult.Error(errorMessage = null) },
628+
onFailure = { throwable ->
629+
when {
630+
throwable.isSslHandShakeError() -> LoginResult.CertificateError
631+
else -> LoginResult.Error(errorMessage = null)
632+
}
633+
},
628634
onSuccess = { it },
629635
)
630636

@@ -1491,9 +1497,12 @@ class AuthRepositoryImpl(
14911497
captchaToken = captchaToken,
14921498
)
14931499
.fold(
1494-
onFailure = {
1495-
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
1496-
false -> LoginResult.UnofficialServerError
1500+
onFailure = { throwable ->
1501+
when {
1502+
throwable.isSslHandShakeError() -> LoginResult.CertificateError
1503+
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
1504+
LoginResult.UnofficialServerError
1505+
}
14971506
else -> LoginResult.Error(errorMessage = null)
14981507
}
14991508
},

app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResult.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ sealed class LoginResult {
2828
* There was an error while logging into an unofficial Bitwarden server.
2929
*/
3030
data object UnofficialServerError : LoginResult()
31+
32+
/**
33+
* There was an error in validating the certificate chain for the server
34+
*/
35+
data object CertificateError : LoginResult()
3136
}

app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util
33
import okio.ByteString.Companion.decodeBase64
44
import java.net.UnknownHostException
55
import java.nio.charset.Charset
6+
import java.security.cert.CertPathValidatorException
67
import java.util.Base64
8+
import javax.net.ssl.SSLHandshakeException
79

810
/**
911
* Base 64 encode the string as well as make special modifications required by the backend:
@@ -41,3 +43,12 @@ fun Throwable?.isNoConnectionError(): Boolean {
4143
return this is UnknownHostException ||
4244
this?.cause?.isNoConnectionError() ?: false
4345
}
46+
47+
/**
48+
* Returns true if the throwable represents a SSL handshake error.
49+
*/
50+
fun Throwable?.isSslHandShakeError(): Boolean {
51+
return this is SSLHandshakeException ||
52+
this is CertPathValidatorException ||
53+
this?.cause?.isSslHandShakeError() ?: false
54+
}

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,17 @@ class EnterpriseSignOnViewModel @Inject constructor(
194194
),
195195
)
196196
}
197+
198+
LoginResult.CertificateError -> {
199+
mutableStateFlow.update {
200+
it.copy(
201+
dialogState = EnterpriseSignOnState.DialogState.Error(
202+
title = R.string.an_error_has_occurred.asText(),
203+
message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
204+
),
205+
)
206+
}
207+
}
197208
}
198209
}
199210

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,17 @@ class LoginViewModel @Inject constructor(
191191
is LoginResult.Success -> {
192192
mutableStateFlow.update { it.copy(dialogState = null) }
193193
}
194+
195+
LoginResult.CertificateError -> {
196+
mutableStateFlow.update {
197+
it.copy(
198+
dialogState = LoginState.DialogState.Error(
199+
title = R.string.an_error_has_occurred.asText(),
200+
message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
201+
),
202+
)
203+
}
204+
}
194205
}
195206
}
196207

@@ -302,7 +313,7 @@ data class LoginState(
302313
*/
303314
@Parcelize
304315
data class Error(
305-
val title: Text?,
316+
val title: Text? = null,
306317
val message: Text,
307318
) : DialogState()
308319

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,17 @@ class LoginWithDeviceViewModel @Inject constructor(
256256
sendEvent(LoginWithDeviceEvent.ShowToast(R.string.login_approved.asText()))
257257
mutableStateFlow.update { it.copy(dialogState = null) }
258258
}
259+
260+
LoginResult.CertificateError -> {
261+
mutableStateFlow.update {
262+
it.copy(
263+
dialogState = LoginWithDeviceState.DialogState.Error(
264+
title = R.string.an_error_has_occurred.asText(),
265+
message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
266+
),
267+
)
268+
}
269+
}
259270
}
260271
}
261272

@@ -452,7 +463,7 @@ data class LoginWithDeviceState(
452463
*/
453464
@Parcelize
454465
data class Error(
455-
val title: Text?,
466+
val title: Text? = null,
456467
val message: Text,
457468
) : DialogState()
458469
}

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,16 @@ class TwoFactorLoginViewModel @Inject constructor(
323323

324324
// NO-OP: Let the auth flow handle navigation after this.
325325
is LoginResult.Success -> Unit
326+
LoginResult.CertificateError -> {
327+
mutableStateFlow.update {
328+
it.copy(
329+
dialogState = TwoFactorLoginState.DialogState.Error(
330+
title = R.string.an_error_has_occurred.asText(),
331+
message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
332+
),
333+
)
334+
}
335+
}
326336
}
327337
}
328338

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutViewModel.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.x8bit.bitwarden.ui.platform.feature.settings.about
22

3+
import android.os.Build
34
import android.os.Parcelable
45
import androidx.lifecycle.SavedStateHandle
56
import androidx.lifecycle.viewModelScope
@@ -91,8 +92,34 @@ class AboutViewModel @Inject constructor(
9192
}
9293

9394
private fun handleVersionClick() {
95+
val buildFlavour = when (BuildConfig.FLAVOR) {
96+
"standard" -> ""
97+
else -> "-${BuildConfig.FLAVOR}"
98+
}
99+
100+
val buildVariant = when (BuildConfig.BUILD_TYPE) {
101+
"debug" -> "dev"
102+
"release" -> "prod"
103+
else -> BuildConfig.BUILD_TYPE
104+
}
105+
106+
val deviceBrandModel = "\uD83D\uDCF1 ${Build.BRAND} ${Build.MODEL}"
107+
val osInfo = "\uD83E\uDD16 ${Build.VERSION.RELEASE}@${Build.VERSION.SDK_INT}"
108+
val buildInfo = "\uD83D\uDCE6 $buildVariant$buildFlavour"
109+
val ciBuildInfoString = BuildConfig.CI_INFO
110+
94111
clipboardManager.setText(
95-
text = state.copyrightInfo.concat("\n\n".asText()).concat(state.version),
112+
text = state.copyrightInfo
113+
.concat("\n\n".asText())
114+
.concat(state.version)
115+
.concat("\n".asText())
116+
.concat("$deviceBrandModel $osInfo $buildInfo".asText())
117+
.concat(
118+
"\n$ciBuildInfoString"
119+
.takeUnless { ciBuildInfoString.isEmpty() }
120+
.orEmpty()
121+
.asText(),
122+
),
96123
)
97124
}
98125

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,4 +1118,5 @@ Do you want to switch to this account?</string>
11181118
<string name="need_some_inspiration">"Need some inspiration?"</string>
11191119
<string name="check_out_the_passphrase_generator">"Check out the passphrase generator"</string>
11201120
<string name="copied_to_clipboard">Copied to clipboard.</string>
1121+
<string name="we_couldnt_verify_the_servers_certificate">We couldn’t verify the server’s certificate. The certificate chain or proxy settings on your device or your Bitwarden server may not be set up correctly.</string>
11211122
</resources>

app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
152152
import org.junit.jupiter.api.BeforeEach
153153
import org.junit.jupiter.api.Test
154154
import java.time.ZonedDateTime
155+
import javax.net.ssl.SSLHandshakeException
155156

156157
@Suppress("LargeClass")
157158
class AuthRepositoryTest {
@@ -1510,6 +1511,43 @@ class AuthRepositoryTest {
15101511
coVerify { identityService.preLogin(email = EMAIL) }
15111512
}
15121513

1514+
@Suppress("MaxLineLength")
1515+
@Test
1516+
fun `login get token fails should return CertificateError when SSLHandshakeException is thrown`() =
1517+
runTest {
1518+
coEvery {
1519+
identityService.preLogin(email = EMAIL)
1520+
} returns PRE_LOGIN_SUCCESS.asSuccess()
1521+
coEvery {
1522+
identityService.getToken(
1523+
email = EMAIL,
1524+
authModel = IdentityTokenAuthModel.MasterPassword(
1525+
username = EMAIL,
1526+
password = PASSWORD_HASH,
1527+
),
1528+
captchaToken = null,
1529+
uniqueAppId = UNIQUE_APP_ID,
1530+
)
1531+
} returns SSLHandshakeException("error").asFailure()
1532+
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
1533+
assertEquals(LoginResult.CertificateError, result)
1534+
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
1535+
coVerify { identityService.preLogin(email = EMAIL) }
1536+
}
1537+
1538+
@Suppress("MaxLineLength")
1539+
@Test
1540+
fun `prelogin fails should return CertificateError when SSLHandshakeException is thrown`() =
1541+
runTest {
1542+
coEvery {
1543+
identityService.preLogin(email = EMAIL)
1544+
} returns SSLHandshakeException("error").asFailure()
1545+
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
1546+
assertEquals(LoginResult.CertificateError, result)
1547+
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
1548+
coVerify { identityService.preLogin(email = EMAIL) }
1549+
}
1550+
15131551
@Test
15141552
fun `login get token returns Invalid should return Error with correct message`() = runTest {
15151553
coEvery {

app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtilsTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import org.junit.jupiter.api.Assertions.assertEquals
44
import org.junit.jupiter.api.Assertions.assertNull
55
import org.junit.jupiter.api.Test
66
import java.net.UnknownHostException
7+
import java.security.cert.CertPathValidatorException
8+
import javax.net.ssl.SSLHandshakeException
79

810
class NetworkUtilsTest {
911
@Test
@@ -57,4 +59,37 @@ class NetworkUtilsTest {
5759
IllegalStateException().isNoConnectionError(),
5860
)
5961
}
62+
63+
@Test
64+
fun `isSslHandshakeError should return return true for SSLHandshakeException`() {
65+
assertEquals(
66+
true,
67+
SSLHandshakeException("whoops").isSslHandShakeError(),
68+
)
69+
}
70+
71+
@Test
72+
fun `isSslHandshakeError should return return true for CertPathValidatorException`() {
73+
assertEquals(
74+
true,
75+
CertPathValidatorException("whoops").isSslHandShakeError(),
76+
)
77+
}
78+
79+
@Suppress("MaxLineLength")
80+
@Test
81+
fun `isSslHandshakeError should return return true if exceptions cause is SSLHandshakeException`() {
82+
assertEquals(
83+
true,
84+
Exception(SSLHandshakeException("whoops")).isSslHandShakeError(),
85+
)
86+
}
87+
88+
@Test
89+
fun `isSslHandshakeError should return return false for not IllegalStateException`() {
90+
assertEquals(
91+
false,
92+
IllegalStateException().isSslHandShakeError(),
93+
)
94+
}
6095
}

0 commit comments

Comments
 (0)