diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt new file mode 100644 index 000000000..ce9293adf --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.api + +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson +import retrofit2.http.Body +import retrofit2.http.PUT +import retrofit2.http.Path + +/** + * Defines raw calls under the /devices API that require authentication. + */ +interface AuthenticatedDevicesApi { + @PUT("/devices/{appId}/keys") + suspend fun updateTrustedDeviceKeys( + @Path(value = "appId") appId: String, + @Body request: TrustedDeviceKeysRequestJson, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt similarity index 74% rename from app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt rename to app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt index be3653113..3190f6248 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt @@ -4,10 +4,9 @@ import retrofit2.http.GET import retrofit2.http.Header /** - * Defines raw calls under the /devices API. + * Defines raw calls under the /devices API that do not require authentication. */ -interface DevicesApi { - +interface UnauthenticatedDevicesApi { @GET("/devices/knowndevice") suspend fun getIsKnownDevice( @Header(value = "X-Request-Email") emailAddress: String, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt index 3afdae6dd..2624401b3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt @@ -54,7 +54,8 @@ object AuthNetworkModule { fun providesDevicesService( retrofits: Retrofits, ): DevicesService = DevicesServiceImpl( - devicesApi = retrofits.unauthenticatedApiRetrofit.create(), + authenticatedDevicesApi = retrofits.authenticatedApiRetrofit.create(), + unauthenticatedDevicesApi = retrofits.unauthenticatedApiRetrofit.create(), ) @Provides diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceKeysRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceKeysRequestJson.kt new file mode 100644 index 000000000..5044e2ffa --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceKeysRequestJson.kt @@ -0,0 +1,14 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * The request body for trusting a device. + */ +@Serializable +data class TrustedDeviceKeysRequestJson( + @SerialName("EncryptedUserKey") val encryptedUserKey: String, + @SerialName("EncryptedPublicKey") val encryptedDevicePublicKey: String, + @SerialName("EncryptedPrivateKey") val encryptedDevicePrivateKey: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceKeysResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceKeysResponseJson.kt new file mode 100644 index 000000000..05ea4acc8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceKeysResponseJson.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.ZonedDateTime + +/** + * The response body for trusting a device. + */ +@Serializable +data class TrustedDeviceKeysResponseJson( + @SerialName("id") val id: String, + @SerialName("name") val name: String, + @SerialName("identifier") val identifier: String, + @SerialName("type") val type: Int, + @Contextual @SerialName("creationDate") val creationDate: ZonedDateTime, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt index bf9889a1d..0149b1bf7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt @@ -1,5 +1,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson + /** * Provides an API for interacting with the /devices endpoints. */ @@ -11,4 +13,14 @@ interface DevicesService { emailAddress: String, deviceId: String, ): Result + + /** + * Establishes trust with this device by storing the encrypted keys in the cloud. + */ + suspend fun trustDevice( + appId: String, + encryptedUserKey: String, + encryptedDevicePublicKey: String, + encryptedDevicePrivateKey: String, + ): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt index eddf700cd..d9efc6336 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt @@ -1,16 +1,34 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service -import com.x8bit.bitwarden.data.auth.datasource.network.api.DevicesApi +import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedDevicesApi +import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevicesApi +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode class DevicesServiceImpl( - private val devicesApi: DevicesApi, + private val authenticatedDevicesApi: AuthenticatedDevicesApi, + private val unauthenticatedDevicesApi: UnauthenticatedDevicesApi, ) : DevicesService { override suspend fun getIsKnownDevice( emailAddress: String, deviceId: String, - ): Result = devicesApi.getIsKnownDevice( + ): Result = unauthenticatedDevicesApi.getIsKnownDevice( emailAddress = emailAddress.base64UrlEncode(), deviceId = deviceId, ) + + override suspend fun trustDevice( + appId: String, + encryptedUserKey: String, + encryptedDevicePublicKey: String, + encryptedDevicePrivateKey: String, + ): Result = authenticatedDevicesApi.updateTrustedDeviceKeys( + appId = appId, + request = TrustedDeviceKeysRequestJson( + encryptedUserKey = encryptedUserKey, + encryptedDevicePublicKey = encryptedDevicePublicKey, + encryptedDevicePrivateKey = encryptedDevicePrivateKey, + ), + ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt index 40f929df3..2ddd6d74e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt @@ -1,18 +1,25 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service -import com.x8bit.bitwarden.data.auth.datasource.network.api.DevicesApi +import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedDevicesApi +import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevicesApi +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson import com.x8bit.bitwarden.data.platform.base.BaseServiceTest +import com.x8bit.bitwarden.data.platform.util.asSuccess import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import retrofit2.create +import java.time.ZonedDateTime class DevicesServiceTest : BaseServiceTest() { - private val devicesApi: DevicesApi = retrofit.create() + private val authenticatedDevicesApi: AuthenticatedDevicesApi = retrofit.create() + private val unauthenticatedDevicesApi: UnauthenticatedDevicesApi = retrofit.create() private val service = DevicesServiceImpl( - devicesApi = devicesApi, + authenticatedDevicesApi = authenticatedDevicesApi, + unauthenticatedDevicesApi = unauthenticatedDevicesApi, ) @Test @@ -30,4 +37,51 @@ class DevicesServiceTest : BaseServiceTest() { val actual = service.getIsKnownDevice("email", "id") assertTrue(actual.isSuccess) } + + @Test + fun `trustDevice when response is Failure should return Failure`() = runTest { + val response = MockResponse().setResponseCode(400) + server.enqueue(response) + val actual = service.trustDevice( + appId = "appId", + encryptedUserKey = "encryptedUserKey", + encryptedDevicePublicKey = "encryptedDevicePublicKey", + encryptedDevicePrivateKey = "encryptedDevicePrivateKey", + ) + assertTrue(actual.isFailure) + } + + @Test + fun `trustDevice when response is Success should return Success`() = runTest { + val response = MockResponse().setBody(TRUST_DEVICE_RESPONSE_JSON).setResponseCode(200) + server.enqueue(response) + val actual = service.trustDevice( + appId = "appId", + encryptedUserKey = "encryptedUserKey", + encryptedDevicePublicKey = "encryptedDevicePublicKey", + encryptedDevicePrivateKey = "encryptedDevicePrivateKey", + ) + assertEquals(TRUST_DEVICE_RESPONSE.asSuccess(), actual) + } } + +private val TRUST_DEVICE_RESPONSE: TrustedDeviceKeysResponseJson = + TrustedDeviceKeysResponseJson( + id = "0d31b6fb-d282-43c7-b614-b13e0129dbd7", + name = "Pixel 8", + identifier = "ea7c0a13-5ce4-4f96-8e17-4fc7fa54f464", + type = 0, + creationDate = ZonedDateTime.parse("2024-03-25T18:04:28.23Z"), + ) + +private const val TRUST_DEVICE_RESPONSE_JSON: String = """ +{ + "id":"0d31b6fb-d282-43c7-b614-b13e0129dbd7", + "name":"Pixel 8", + "type":0, + "identifier":"ea7c0a13-5ce4-4f96-8e17-4fc7fa54f464", + "creationDate":"2024-03-25T18:04:28.23Z", + "isTrusted":true, + "object":"device" +} +"""