diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt index 9b0e93fe16..18909c7976 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt @@ -91,5 +91,8 @@ object PlatformNetworkModule { serializersModule = SerializersModule { contextual(ZonedDateTimeSerializer()) } + + // Respect model default property values. + coerceInputValues = true } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt index fc14a49855..d40678b869 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt @@ -6,6 +6,10 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject import java.time.ZonedDateTime +private const val DEFAULT_FIDO_2_KEY_TYPE = "public-key" +private const val DEFAULT_FIDO_2_KEY_ALGORITHM = "ECDSA" +private const val DEFAULT_FIDO_2_KEY_CURVE = "P-256" + /** * Represents the response model for vault data fetched from the server. * @@ -731,6 +735,9 @@ data class SyncResponseJson( @SerialName("username") val username: String?, + + @SerialName("fido2Credentials") + val fido2Credentials: List?, ) { /** * Represents a URI in the vault response. @@ -774,6 +781,66 @@ data class SyncResponseJson( @SerialName("type") val type: SecureNoteTypeJson, ) + + /** + * Represents a FIDO2 credential object in the vault response. + * + * @property credentialId The unique identifier of the FIDO2 credential. + * @property keyType The type of public key of the FIDO2 credential. + * @property keyAlgorithm The public Key algorithm of the credential. + * @property keyValue The public key of the credential. + * @property rpId The relying party (RP) identity. + * @property rpName The optional name of the relying party (RP). + * @property userHandle The optional unique identifier used to identify an account. + * @property userName The conditional, formal name of the user associated to the credential. + * @property userDisplayName The optional display name of the user associated to the + * credential. + * @property counter The signature counter for the credential. + * @property discoverable Whether the FIDO2 credential is discoverable or non-discoverable. + * @property creationDate The creation date and time of the credential. + */ + @Serializable + data class Fido2Credential( + @SerialName("credentialId") + val credentialId: String, + + @SerialName("keyType") + val keyType: String = DEFAULT_FIDO_2_KEY_TYPE, + + @SerialName("keyAlgorithm") + val keyAlgorithm: String = DEFAULT_FIDO_2_KEY_ALGORITHM, + + @SerialName("keyCurve") + val keyCurve: String = DEFAULT_FIDO_2_KEY_CURVE, + + @SerialName("keyValue") + val keyValue: String, + + @SerialName("rpId") + val rpId: String, + + @SerialName("rpName") + val rpName: String?, + + @SerialName("userHandle") + val userHandle: String?, + + @SerialName("userName") + val userName: String?, + + @SerialName("userDisplayName") + val userDisplayName: String?, + + @SerialName("counter") + val counter: String, + + @SerialName("discoverable") + val discoverable: String, + + @SerialName("creationDate") + @Contextual + val creationDate: ZonedDateTime, + ) } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt index 2da0cad4fd..de48edc8d3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt @@ -8,6 +8,7 @@ import com.bitwarden.core.Cipher import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView +import com.bitwarden.core.Fido2Credential import com.bitwarden.core.Field import com.bitwarden.core.FieldType import com.bitwarden.core.Identity @@ -232,8 +233,28 @@ private fun Login.toEncryptedNetworkLogin(): SyncResponseJson.Cipher.Login = // uri needs to be null to avoid duplicating the first url entry for a login item. uri = null, username = username, + fido2Credentials = fido2Credentials?.toNetworkFido2Credentials(), ) +private fun List.toNetworkFido2Credentials() = + this.map { it.toNetworkFido2Credential() } + +private fun Fido2Credential.toNetworkFido2Credential() = SyncResponseJson.Cipher.Fido2Credential( + credentialId = credentialId, + keyType = keyType, + keyAlgorithm = keyAlgorithm, + keyCurve = keyCurve, + keyValue = keyValue, + rpId = rpId, + rpName = rpName, + userHandle = userHandle, + userName = userName, + userDisplayName = userDisplayName, + counter = counter, + discoverable = discoverable, + creationDate = ZonedDateTime.ofInstant(creationDate, ZoneOffset.UTC), +) + /** * Converts a list of Bitwarden SDK [PasswordHistory] objects to a corresponding * list of [SyncResponseJson.Cipher.PasswordHistory] objects. @@ -325,8 +346,28 @@ fun SyncResponseJson.Cipher.Login.toSdkLogin(): Login = uris = uris?.toSdkLoginUriList(), totp = totp, autofillOnPageLoad = shouldAutofillOnPageLoad, + fido2Credentials = fido2Credentials?.toSdkFido2Credentials(), ) +private fun List.toSdkFido2Credentials() = + this.map { it.toSdkFido2Credential() } + +private fun SyncResponseJson.Cipher.Fido2Credential.toSdkFido2Credential() = Fido2Credential( + credentialId = credentialId, + keyType = keyType, + keyAlgorithm = keyAlgorithm, + keyCurve = keyCurve, + keyValue = keyValue, + rpId = rpId, + rpName = rpName, + userHandle = userHandle, + userName = userName, + userDisplayName = userDisplayName, + counter = counter, + discoverable = discoverable, + creationDate = creationDate.toInstant(), +) + /** * Transforms a [SyncResponseJson.Cipher.Identity] into the corresponding Bitwarden SDK [Identity]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt index 26c1199685..486c5ff39d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt @@ -147,6 +147,7 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toLoginView( uris = it.uriList.toLoginUriView(), totp = it.totp, autofillOnPageLoad = common.originalCipher?.login?.autofillOnPageLoad, + fido2Credentials = common.originalCipher?.login?.fido2Credentials, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index b1ff536698..6b701877cc 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -337,7 +337,24 @@ private const val CIPHER_JSON = """ "passwordRevisionDate": "2023-10-27T12:00:00.000Z", "autofillOnPageLoad": false, "uri": "mockUri-1", - "username": "mockUsername-1" + "username": "mockUsername-1", + "fido2Credentials": [ + { + "credentialId": "mockCredentialId-1", + "keyType": "mockKeyType-1", + "keyAlgorithm": "mockKeyAlgorithm-1", + "keyCurve": "mockKeyCurve-1", + "keyValue": "mockKeyValue-1", + "rpId": "mockRpId-1", + "rpName": "mockRpName-1", + "userHandle": "mockUserHandle-1", + "userName": "mockUserName-1", + "userDisplayName": "mockUserDisplayName-1", + "counter": "mockCounter-1", + "discoverable": "mockDiscoverable-1", + "creationDate": "2024-03-12T20:20:16.456Z" + } + ] }, "creationDate": "2023-10-27T12:00:00.000Z", "secureNote": { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt index 46a58b7ea7..ccc11d5348 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt @@ -120,11 +120,28 @@ fun createMockLogin(number: Int, hasNullUri: Boolean = false): SyncResponseJson. password = "mockPassword-$number", passwordRevisionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), shouldAutofillOnPageLoad = false, - uri = if (hasNullUri) { null } else "mockUri-$number", + uri = if (hasNullUri) null else "mockUri-$number", uris = listOf(createMockUri(number = number)), totp = "mockTotp-$number", + fido2Credentials = listOf(createMockFido2Credential(number)), ) +fun createMockFido2Credential(number: Int) = SyncResponseJson.Cipher.Fido2Credential( + credentialId = "mockCredentialId-$number", + keyType = "mockKeyType-$number", + keyAlgorithm = "mockKeyAlgorithm-$number", + keyCurve = "mockKeyCurve-$number", + keyValue = "mockKeyValue-$number", + rpId = "mockRpId-$number", + rpName = "mockRpName-$number", + userHandle = "mockUserHandle-$number", + userName = "mockUserName-$number", + userDisplayName = "mockUserDisplayName-$number", + counter = "mockCounter-$number", + discoverable = "mockDiscoverable-$number", + creationDate = ZonedDateTime.parse("2024-03-12T20:20:16.456Z"), +) + /** * Create a mock [SyncResponseJson.Cipher.Login.Uri] with a given [number]. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt index 6fa22aa815..ecf0c7f0dd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt @@ -314,7 +314,24 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """ "passwordRevisionDate": "2023-10-27T12:00:00.00Z", "autofillOnPageLoad": false, "uri": "mockUri-1", - "username": "mockUsername-1" + "username": "mockUsername-1", + "fido2Credentials": [ + { + "credentialId": "mockCredentialId-1", + "keyType": "mockKeyType-1", + "keyAlgorithm": "mockKeyAlgorithm-1", + "keyCurve": "mockKeyCurve-1", + "keyValue": "mockKeyValue-1", + "rpId": "mockRpId-1", + "rpName": "mockRpName-1", + "userHandle": "mockUserHandle-1", + "userName": "mockUserName-1", + "userDisplayName": "mockUserDisplayName-1", + "counter": "mockCounter-1", + "discoverable": "mockDiscoverable-1", + "creationDate": "2024-03-12T20:20:16.456Z" + } + ] }, "creationDate": "2023-10-27T12:00:00.00Z", "secureNote": { @@ -407,7 +424,24 @@ private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """ "passwordRevisionDate": "2023-10-27T12:00:00.00Z", "autofillOnPageLoad": false, "uri": "mockUri-1", - "username": "mockUsername-1" + "username": "mockUsername-1", + "fido2Credentials": [ + { + "credentialId": "mockCredentialId-1", + "keyType": "mockKeyType-1", + "keyAlgorithm": "mockKeyAlgorithm-1", + "keyCurve": "mockKeyCurve-1", + "keyValue": "mockKeyValue-1", + "rpId": "mockRpId-1", + "rpName": "mockRpName-1", + "userHandle": "mockUserHandle-1", + "userName": "mockUserName-1", + "userDisplayName": "mockUserDisplayName-1", + "counter": "mockCounter-1", + "discoverable": "mockDiscoverable-1", + "creationDate": "2024-03-12T20:20:16.456Z" + } + ] }, "creationDate": "2023-10-27T12:00:00.00Z", "secureNote": { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt index 54848e595d..e58f134590 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt @@ -230,7 +230,24 @@ private const val SYNC_SUCCESS_JSON = """ "passwordRevisionDate": "2023-10-27T12:00:00.00Z", "autofillOnPageLoad": false, "uri": "mockUri-1", - "username": "mockUsername-1" + "username": "mockUsername-1", + "fido2Credentials": [ + { + "credentialId": "mockCredentialId-1", + "keyType": "mockKeyType-1", + "keyAlgorithm": "mockKeyAlgorithm-1", + "keyCurve": "mockKeyCurve-1", + "keyValue": "mockKeyValue-1", + "rpId": "mockRpId-1", + "rpName": "mockRpName-1", + "userHandle": "mockUserHandle-1", + "userName": "mockUserName-1", + "userDisplayName": "mockUserDisplayName-1", + "counter": "mockCounter-1", + "discoverable": "mockDiscoverable-1", + "creationDate": "2024-03-12T20:20:16.456Z" + } + ] }, "creationDate": "2023-10-27T12:00:00.00Z", "secureNote": { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index 1ddf056d29..dea9df65ec 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -5,6 +5,7 @@ import com.bitwarden.core.CardView import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView +import com.bitwarden.core.Fido2Credential import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView import com.bitwarden.core.IdentityView @@ -89,6 +90,29 @@ fun createMockLoginView( autofillOnPageLoad = false, uris = listOf(createMockUriView(number = number)), totp = totp, + fido2Credentials = createMockSdkFido2CredentialList(number), + ) + +fun createMockSdkFido2CredentialList(number: Int) = + listOf(createMockSdkFido2CredentialView(number)) + +fun createMockSdkFido2CredentialView(number: Int) = + Fido2Credential( + credentialId = "mockCredentialId-$number", + keyType = "mockKeyType-$number", + keyAlgorithm = "mockKeyAlgorithm-$number", + keyCurve = "mockKeyCurve-$number", + keyValue = "mockKeyValue-$number", + rpId = "mockRpId-$number", + userHandle = "mockUserHandle-$number", + userName = "mockUserName-$number", + counter = "mockCounter-$number", + rpName = "mockRpName-$number", + userDisplayName = "mockUserDisplayName-$number", + discoverable = "mockDiscoverable-$number", + creationDate = ZonedDateTime + .parse("2024-03-12T20:20:16.456Z") + .toInstant(), ) /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt index bb845cebe0..9572ef26e9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt @@ -147,6 +147,7 @@ fun createMockSdkLogin(number: Int): Login = autofillOnPageLoad = false, uris = listOf(createMockSdkUri(number = number)), totp = "mockTotp-$number", + fido2Credentials = createMockSdkFido2CredentialList(number), ) /** diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index c826cc67cf..0eb17e625d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -20,6 +20,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState @@ -529,6 +530,7 @@ private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.cop ), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", autofillOnPageLoad = false, + fido2Credentials = createMockSdkFido2CredentialList(number = 1), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index fd35d2b0b3..14a4635eac 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -10,6 +10,7 @@ import com.bitwarden.core.IdentityView import com.bitwarden.core.LoginUriView import com.bitwarden.core.LoginView import com.bitwarden.core.PasswordHistoryView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType @@ -42,6 +43,7 @@ fun createLoginView(isEmpty: Boolean): LoginView = totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" .takeUnless { isEmpty }, autofillOnPageLoad = false, + fido2Credentials = createMockSdkFido2CredentialList(number = 1), ) @Suppress("CyclomaticComplexMethod") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt index 5d8b97ab04..5f4001e86e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt @@ -84,6 +84,7 @@ class VaultAddItemStateExtensionsTest { ), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", autofillOnPageLoad = null, + fido2Credentials = null, ), identity = null, card = null, @@ -162,6 +163,7 @@ class VaultAddItemStateExtensionsTest { ), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", autofillOnPageLoad = false, + fido2Credentials = null, ), favorite = true, reprompt = CipherRepromptType.NONE, @@ -741,6 +743,7 @@ private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.cop ), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", autofillOnPageLoad = false, + fido2Credentials = null, ), ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f15db1a1e..64e221cfd2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ androidxSplash = "1.1.0-alpha02" androidXAppCompat = "1.6.1" androdixAutofill = "1.1.0" androidxWork = "2.9.0" -bitwardenSdk = "0.4.0-20240306.185653-166" +bitwardenSdk = "0.4.0-20240314.115913-173" crashlytics = "2.9.9" detekt = "1.23.5" firebaseBom = "32.7.3"