Add trusted device API (#1183)

This commit is contained in:
David Perez 2024-03-28 13:07:02 -05:00 committed by Álison Fernandes
parent 9253a7b682
commit de6f31775b
8 changed files with 144 additions and 10 deletions

View file

@ -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<TrustedDeviceKeysResponseJson>
}

View file

@ -4,10 +4,9 @@ import retrofit2.http.GET
import retrofit2.http.Header 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") @GET("/devices/knowndevice")
suspend fun getIsKnownDevice( suspend fun getIsKnownDevice(
@Header(value = "X-Request-Email") emailAddress: String, @Header(value = "X-Request-Email") emailAddress: String,

View file

@ -54,7 +54,8 @@ object AuthNetworkModule {
fun providesDevicesService( fun providesDevicesService(
retrofits: Retrofits, retrofits: Retrofits,
): DevicesService = DevicesServiceImpl( ): DevicesService = DevicesServiceImpl(
devicesApi = retrofits.unauthenticatedApiRetrofit.create(), authenticatedDevicesApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedDevicesApi = retrofits.unauthenticatedApiRetrofit.create(),
) )
@Provides @Provides

View file

@ -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,
)

View file

@ -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,
)

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service 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. * Provides an API for interacting with the /devices endpoints.
*/ */
@ -11,4 +13,14 @@ interface DevicesService {
emailAddress: String, emailAddress: String,
deviceId: String, deviceId: String,
): Result<Boolean> ): Result<Boolean>
/**
* 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<TrustedDeviceKeysResponseJson>
} }

View file

@ -1,16 +1,34 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service 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 import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
class DevicesServiceImpl( class DevicesServiceImpl(
private val devicesApi: DevicesApi, private val authenticatedDevicesApi: AuthenticatedDevicesApi,
private val unauthenticatedDevicesApi: UnauthenticatedDevicesApi,
) : DevicesService { ) : DevicesService {
override suspend fun getIsKnownDevice( override suspend fun getIsKnownDevice(
emailAddress: String, emailAddress: String,
deviceId: String, deviceId: String,
): Result<Boolean> = devicesApi.getIsKnownDevice( ): Result<Boolean> = unauthenticatedDevicesApi.getIsKnownDevice(
emailAddress = emailAddress.base64UrlEncode(), emailAddress = emailAddress.base64UrlEncode(),
deviceId = deviceId, deviceId = deviceId,
) )
override suspend fun trustDevice(
appId: String,
encryptedUserKey: String,
encryptedDevicePublicKey: String,
encryptedDevicePrivateKey: String,
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi.updateTrustedDeviceKeys(
appId = appId,
request = TrustedDeviceKeysRequestJson(
encryptedUserKey = encryptedUserKey,
encryptedDevicePublicKey = encryptedDevicePublicKey,
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
),
)
} }

View file

@ -1,18 +1,25 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service 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.base.BaseServiceTest
import com.x8bit.bitwarden.data.platform.util.asSuccess
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import retrofit2.create import retrofit2.create
import java.time.ZonedDateTime
class DevicesServiceTest : BaseServiceTest() { 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( private val service = DevicesServiceImpl(
devicesApi = devicesApi, authenticatedDevicesApi = authenticatedDevicesApi,
unauthenticatedDevicesApi = unauthenticatedDevicesApi,
) )
@Test @Test
@ -30,4 +37,51 @@ class DevicesServiceTest : BaseServiceTest() {
val actual = service.getIsKnownDevice("email", "id") val actual = service.getIsKnownDevice("email", "id")
assertTrue(actual.isSuccess) 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"
}
"""