Skip to content

Commit b74d3b1

Browse files
Merge branch 'main' into QA-1126b/adding-native-sanity-test
2 parents a6efb31 + 9ed59e6 commit b74d3b1

58 files changed

Lines changed: 3482 additions & 2231 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class UserLogoutManagerImpl(
8080
// Save any data that will still need to be retained after otherwise clearing all dat
8181
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
8282
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
83+
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
8384

8485
switchUserIfAvailable(
8586
currentUserId = userId,
@@ -101,6 +102,10 @@ class UserLogoutManagerImpl(
101102
vaultTimeoutAction = vaultTimeoutAction,
102103
)
103104
}
105+
authDiskSource.storePinProtectedUserKey(
106+
userId = userId,
107+
pinProtectedUserKey = pinProtectedUserKey,
108+
)
104109
}
105110

106111
private fun clearData(userId: String) {

app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/provider/AutofillCipherProviderImpl.kt

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package com.x8bit.bitwarden.data.autofill.provider
22

3+
import com.bitwarden.vault.CipherListView
4+
import com.bitwarden.vault.CipherListViewType
35
import com.bitwarden.vault.CipherRepromptType
4-
import com.bitwarden.vault.CipherType
56
import com.bitwarden.vault.CipherView
67
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
78
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
89
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
910
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
1011
import com.x8bit.bitwarden.data.platform.util.subtitle
12+
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
1113
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
1214
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
1315
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
16+
import timber.log.Timber
1417

1518
/**
1619
* The duration, in milliseconds, we should wait while waiting for the vault status to not be
@@ -49,44 +52,48 @@ class AutofillCipherProviderImpl(
4952
}
5053

5154
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
52-
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
55+
val cipherListViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
5356

54-
return cipherViews
55-
.mapNotNull { cipherView ->
56-
cipherView
57+
return cipherListViews
58+
.mapNotNull { cipherListView ->
59+
cipherListView
5760
// We only care about non-deleted card ciphers.
5861
.takeIf {
5962
// Must be card type.
60-
cipherView.type == CipherType.CARD &&
63+
it.type is CipherListViewType.Card &&
6164
// Must not be deleted.
62-
cipherView.deletedDate == null &&
65+
it.deletedDate == null &&
6366
// Must not require a reprompt.
6467
it.reprompt == CipherRepromptType.NONE
6568
}
66-
?.let { nonNullCipherView ->
67-
AutofillCipher.Card(
68-
cipherId = cipherView.id,
69-
name = nonNullCipherView.name,
70-
subtitle = nonNullCipherView.subtitle.orEmpty(),
71-
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
72-
code = nonNullCipherView.card?.code.orEmpty(),
73-
expirationMonth = nonNullCipherView.card?.expMonth.orEmpty(),
74-
expirationYear = nonNullCipherView.card?.expYear.orEmpty(),
75-
number = nonNullCipherView.card?.number.orEmpty(),
76-
)
69+
?.let { nonNullCipherListView ->
70+
nonNullCipherListView.id?.let { cipherId ->
71+
decryptCipherOrNull(cipherId = cipherId)?.let { cipherView ->
72+
AutofillCipher.Card(
73+
cipherId = cipherView.id,
74+
name = cipherView.name,
75+
subtitle = cipherView.subtitle.orEmpty(),
76+
cardholderName = cipherView.card?.cardholderName.orEmpty(),
77+
code = cipherView.card?.code.orEmpty(),
78+
expirationMonth = cipherView.card?.expMonth.orEmpty(),
79+
expirationYear = cipherView.card?.expYear.orEmpty(),
80+
number = cipherView.card?.number.orEmpty(),
81+
)
82+
}
83+
}
7784
}
7885
}
7986
}
8087

8188
override suspend fun getLoginAutofillCiphers(
8289
uri: String,
8390
): List<AutofillCipher.Login> {
84-
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
91+
val cipherViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
8592
// We only care about non-deleted login ciphers.
8693
val loginCiphers = cipherViews
8794
.filter {
8895
// Must be login type
89-
it.type == CipherType.LOGIN &&
96+
it.type is CipherListViewType.Login &&
9097
// Must not be deleted.
9198
it.deletedDate == null &&
9299
// Must not require a reprompt.
@@ -96,9 +103,12 @@ class AutofillCipherProviderImpl(
96103
return cipherMatchingManager
97104
// Filter for ciphers that match the uri in some way.
98105
.filterCiphersForMatches(
99-
ciphers = loginCiphers,
106+
cipherListViews = loginCiphers,
100107
matchUri = uri,
101108
)
109+
.mapNotNull { cipherListView ->
110+
cipherListView.id?.let { decryptCipherOrNull(cipherId = it) }
111+
}
102112
.map { cipherView ->
103113
AutofillCipher.Login(
104114
cipherId = cipherView.id,
@@ -114,10 +124,24 @@ class AutofillCipherProviderImpl(
114124
/**
115125
* Get available [CipherView]s if possible.
116126
*/
117-
private suspend fun getUnlockedCiphersOrNull(): List<CipherView>? =
127+
private suspend fun getUnlockedCipherListViewsOrNull(): List<CipherListView>? =
118128
vaultRepository
119-
.ciphersStateFlow
129+
.decryptCipherListResultStateFlow
120130
.takeUnless { isVaultLocked() }
121131
?.firstWithTimeoutOrNull(timeMillis = GET_CIPHERS_TIMEOUT_MS) { it.data != null }
122132
?.data
133+
?.successes
134+
135+
private suspend fun decryptCipherOrNull(cipherId: String): CipherView? =
136+
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
137+
GetCipherResult.CipherNotFound -> {
138+
Timber.e("Cipher not found for autofill.")
139+
null
140+
}
141+
is GetCipherResult.Failure -> {
142+
Timber.e(result.error, "Failed to decrypt cipher for autofill.")
143+
null
144+
}
145+
is GetCipherResult.Success -> result.cipherView
146+
}
123147
}

app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/CipherListViewExtensions.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import com.bitwarden.vault.CipherListView
55
import com.bitwarden.vault.CipherListViewType
66
import com.bitwarden.vault.LoginListView
77

8+
/**
9+
* Returns true when the cipher is not deleted and contains at least one FIDO 2 credential.
10+
*/
11+
val CipherListView.isActiveWithFido2Credentials: Boolean
12+
get() = deletedDate == null && login?.hasFido2 ?: false
13+
814
/**
915
* Returns the [LoginListView] if the cipher is of type [CipherListViewType.Login], otherwise null.
1016
*/

app/src/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.bitwarden.fido.Origin
1818
import com.bitwarden.fido.UnverifiedAssetLink
1919
import com.bitwarden.sdk.Fido2CredentialStore
2020
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
21+
import com.bitwarden.vault.CipherListView
2122
import com.bitwarden.vault.CipherView
2223
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
2324
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
@@ -35,8 +36,8 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
3536
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
3637
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
3738
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
39+
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
3840
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
39-
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
4041
import kotlinx.coroutines.CoroutineScope
4142
import kotlinx.coroutines.flow.fold
4243
import kotlinx.coroutines.withContext
@@ -168,28 +169,23 @@ class BitwardenCredentialManagerImpl(
168169
override suspend fun getCredentialEntries(
169170
getCredentialsRequest: GetCredentialsRequest,
170171
): Result<List<CredentialEntry>> = withContext(ioScope.coroutineContext) {
171-
val cipherViews = vaultRepository
172-
.ciphersStateFlow
172+
val cipherListViews = vaultRepository
173+
.decryptCipherListResultStateFlow
173174
.takeUntilLoaded()
174-
.fold(initial = emptyList<CipherView>()) { _, dataState ->
175+
.fold(initial = emptyList<CipherListView>()) { _, dataState ->
175176
when (dataState) {
176-
is DataState.Loaded -> {
177-
dataState.data
178-
}
179-
177+
is DataState.Loaded -> dataState.data.successes
180178
else -> emptyList()
181179
}
182180
}
183181
.filter { it.isActiveWithFido2Credentials }
184-
.ifEmpty {
185-
return@withContext emptyList<CredentialEntry>().asSuccess()
186-
}
182+
.ifEmpty { return@withContext emptyList<CredentialEntry>().asSuccess() }
187183

188184
getCredentialsRequest
189185
.beginGetPublicKeyCredentialOptions
190186
.toPublicKeyCredentialEntries(
191187
userId = getCredentialsRequest.userId,
192-
cipherViewsWithPublicKeyCredentials = cipherViews,
188+
cipherListViews = cipherListViews,
193189
)
194190
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
195191
}
@@ -200,7 +196,7 @@ class BitwardenCredentialManagerImpl(
200196

201197
private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
202198
userId: String,
203-
cipherViewsWithPublicKeyCredentials: List<CipherView>,
199+
cipherListViews: List<CipherListView>,
204200
): Result<List<CredentialEntry>> {
205201
val relyingPartyIds = this
206202
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
@@ -209,27 +205,49 @@ class BitwardenCredentialManagerImpl(
209205
return GetCredentialUnknownException("Relying party id required.").asFailure()
210206
}
211207

212-
val decryptResult = vaultRepository
213-
.getDecryptedFido2CredentialAutofillViews(cipherViewsWithPublicKeyCredentials)
208+
val cipherViews = cipherListViews
209+
.mapNotNull { cipherListView ->
210+
when (val result = vaultRepository.getCipher(cipherListView.id.orEmpty())) {
211+
GetCipherResult.CipherNotFound -> {
212+
Timber.e("Cipher not found while building public key credential entries.")
213+
null
214+
}
214215

215-
return when (decryptResult) {
216-
is DecryptFido2CredentialAutofillViewResult.Error -> {
217-
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
218-
}
216+
is GetCipherResult.Failure -> {
217+
Timber.e(
218+
result.error,
219+
"Failed to decrypt cipher while building credential entries.",
220+
)
221+
null
222+
}
219223

220-
is DecryptFido2CredentialAutofillViewResult.Success -> {
221-
credentialEntryBuilder
222-
.buildPublicKeyCredentialEntries(
223-
userId = userId,
224-
fido2CredentialAutofillViews = decryptResult
225-
.fido2CredentialAutofillViews
226-
.filter { it.rpId in relyingPartyIds },
227-
beginGetPublicKeyCredentialOptions = this,
228-
isUserVerified = isUserVerified,
229-
)
230-
.asSuccess()
224+
is GetCipherResult.Success -> result.cipherView
225+
}
231226
}
232-
}
227+
.toTypedArray()
228+
.ifEmpty { return emptyList<CredentialEntry>().asSuccess() }
229+
230+
return vaultSdkSource
231+
.decryptFido2CredentialAutofillViews(
232+
userId = userId,
233+
cipherViews = cipherViews,
234+
)
235+
.fold(
236+
onSuccess = { fido2AutofillViews ->
237+
credentialEntryBuilder
238+
.buildPublicKeyCredentialEntries(
239+
userId = userId,
240+
fido2CredentialAutofillViews = fido2AutofillViews
241+
.filter { it.rpId in relyingPartyIds },
242+
beginGetPublicKeyCredentialOptions = this,
243+
isUserVerified = isUserVerified,
244+
)
245+
.asSuccess()
246+
},
247+
onFailure = {
248+
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
249+
},
250+
)
233251
}
234252

235253
private suspend fun registerFido2CredentialForUnprivilegedApp(
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
22

3-
import com.bitwarden.vault.CipherView
3+
import com.bitwarden.vault.CipherListView
44

55
/**
66
* A manager for matching ciphers based on special criteria.
77
*/
88
interface CipherMatchingManager {
99
/**
10-
* Filter [ciphers] for entries that match the [matchUri] in some fashion.
10+
* Filter [cipherListViews] for entries that match the [matchUri] in some fashion.
1111
*/
1212
suspend fun filterCiphersForMatches(
13-
ciphers: List<CipherView>,
13+
cipherListViews: List<CipherListView>,
1414
matchUri: String,
15-
): List<CipherView>
15+
): List<CipherListView>
1616
}

app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/ciphermatching/CipherMatchingManagerImpl.kt

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
22

3-
import com.bitwarden.vault.CipherView
3+
import com.bitwarden.vault.CipherListView
44
import com.bitwarden.vault.LoginUriView
55
import com.bitwarden.vault.UriMatchType
6+
import com.x8bit.bitwarden.data.autofill.util.login
67
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
78
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
89
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
@@ -33,9 +34,9 @@ class CipherMatchingManagerImpl(
3334
private val vaultRepository: VaultRepository,
3435
) : CipherMatchingManager {
3536
override suspend fun filterCiphersForMatches(
36-
ciphers: List<CipherView>,
37+
cipherListViews: List<CipherListView>,
3738
matchUri: String,
38-
): List<CipherView> {
39+
): List<CipherListView> {
3940
val equivalentDomainsData = vaultRepository
4041
.domainsStateFlow
4142
.mapNotNull { it.data }
@@ -58,23 +59,23 @@ class CipherMatchingManagerImpl(
5859
matchUri = matchUri,
5960
)
6061

61-
val exactMatchingCiphers = mutableListOf<CipherView>()
62-
val fuzzyMatchingCiphers = mutableListOf<CipherView>()
62+
val exactMatchingCiphers = mutableListOf<CipherListView>()
63+
val fuzzyMatchingCiphers = mutableListOf<CipherListView>()
6364

64-
ciphers
65-
.forEach { cipherView ->
65+
cipherListViews
66+
.forEach { cipherListView ->
6667
val matchResult = checkForCipherMatch(
6768
resourceCacheManager = resourceCacheManager,
68-
cipherView = cipherView,
69+
cipherListView = cipherListView,
6970
defaultUriMatchType = defaultUriMatchType,
7071
isAndroidApp = isAndroidApp,
7172
matchUri = matchUri,
7273
matchingDomains = matchingDomains,
7374
)
7475

7576
when (matchResult) {
76-
MatchResult.EXACT -> exactMatchingCiphers.add(cipherView)
77-
MatchResult.FUZZY -> fuzzyMatchingCiphers.add(cipherView)
77+
MatchResult.EXACT -> exactMatchingCiphers.add(cipherListView)
78+
MatchResult.FUZZY -> fuzzyMatchingCiphers.add(cipherListView)
7879
MatchResult.NONE -> Unit
7980
}
8081
}
@@ -135,10 +136,10 @@ private fun getMatchingDomains(
135136
}
136137

137138
/**
138-
* Check to see if [cipherView] matches [matchUri] in some way. The returned [MatchResult] will
139+
* Check to see if [cipherListView] matches [matchUri] in some way. The returned [MatchResult] will
139140
* provide details on the match quality.
140141
*
141-
* @param cipherView The cipher to be judged for a match.
142+
* @param cipherListView The cipher to be judged for a match.
142143
* @param resourceCacheManager The [ResourceCacheManager] for fetching cached resources.
143144
* @param defaultUriMatchType The global default [UriMatchType].
144145
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
@@ -148,13 +149,13 @@ private fun getMatchingDomains(
148149
@Suppress("LongParameterList")
149150
private fun checkForCipherMatch(
150151
resourceCacheManager: ResourceCacheManager,
151-
cipherView: CipherView,
152+
cipherListView: CipherListView,
152153
defaultUriMatchType: UriMatchType,
153154
isAndroidApp: Boolean,
154155
matchingDomains: MatchingDomains,
155156
matchUri: String,
156157
): MatchResult {
157-
val matchResults = cipherView
158+
val matchResults = cipherListView
158159
.login
159160
?.uris
160161
?.map { loginUriView ->

0 commit comments

Comments
 (0)