BIT-325: Create and persist device identifier (#298)

Co-authored-by: Oleg Semenenko <oleg@livefront.com>
This commit is contained in:
Brian Yencho 2023-11-29 16:59:06 -06:00 committed by Álison Fernandes
parent aec33a4d07
commit c9c313230f
9 changed files with 71 additions and 3 deletions

View file

@ -7,6 +7,13 @@ import kotlinx.coroutines.flow.Flow
* Primary access point for disk information.
*/
interface AuthDiskSource {
/**
* Retrieves a unique ID for the application that is stored locally. This will generate a new
* one if it does not yet exist and it will only be reset for new installs or when clearing
* application data.
*/
val uniqueAppId: String
/**
* The currently persisted saved email address (or `null` if not set).
*/

View file

@ -9,7 +9,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID
private const val UNIQUE_APP_ID_KEY = "$BASE_KEY:appId"
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
private const val STATE_KEY = "$BASE_KEY:state"
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey"
@ -23,6 +25,9 @@ class AuthDiskSourceImpl(
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
AuthDiskSource {
override val uniqueAppId: String
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
override var rememberedEmailAddress: String?
get() = getString(key = REMEMBERED_EMAIL_ADDRESS_KEY)
set(value) {
@ -70,4 +75,12 @@ class AuthDiskSourceImpl(
value = privateKey,
)
}
private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()
.toString()
.also {
putString(key = UNIQUE_APP_ID_KEY, value = it)
}
}

View file

@ -11,11 +11,13 @@ interface IdentityService {
/**
* Make request to get an access token.
*
* @param uniqueAppId applications unique identifier.
* @param email user's email address.
* @param passwordHash password hashed with the Bitwarden SDK.
* @param captchaToken captcha token to be passed to the API (nullable).
*/
suspend fun getToken(
uniqueAppId: String,
email: String,
passwordHash: String,
captchaToken: String?,

View file

@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResul
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
import kotlinx.serialization.json.Json
import java.util.UUID
class IdentityServiceImpl constructor(
private val api: IdentityApi,
@ -19,6 +18,7 @@ class IdentityServiceImpl constructor(
@Suppress("MagicNumber")
override suspend fun getToken(
uniqueAppId: String,
email: String,
passwordHash: String,
captchaToken: String?,
@ -27,8 +27,7 @@ class IdentityServiceImpl constructor(
scope = "api+offline_access",
clientId = "mobile",
authEmail = email.base64UrlEncode(),
// TODO: use correct device identifier here BIT-325
deviceIdentifier = UUID.randomUUID().toString(),
deviceIdentifier = uniqueAppId,
deviceName = deviceModelProvider.deviceModel,
deviceType = "0",
grantType = "password",

View file

@ -135,6 +135,7 @@ class AuthRepositoryImpl constructor(
}
.flatMap { passwordHash ->
identityService.getToken(
uniqueAppId = authDiskSource.uniqueAppId,
email = email,
passwordHash = passwordHash,
captchaToken = captchaToken,

View file

@ -31,6 +31,34 @@ class AuthDiskSourceTest {
json = json,
)
@Test
fun `uniqueAppId should generate a new ID and update SharedPreferences if none exists`() {
val rememberedUniqueAppIdKey = "bwPreferencesStorage:appId"
// Assert that the SharedPreferences are empty
assertNull(fakeSharedPreferences.getString(rememberedUniqueAppIdKey, null))
// Generate a new uniqueAppId and retrieve it
val newId = authDiskSource.uniqueAppId
// Ensure that the SharedPreferences were updated
assertEquals(
newId,
fakeSharedPreferences.getString(rememberedUniqueAppIdKey, null),
)
}
@Test
fun `uniqueAppId should not generate a new ID if one exists`() {
val rememberedUniqueAppIdKey = "bwPreferencesStorage:appId"
val testId = "testId"
// Update preferences to hold test value
fakeSharedPreferences.edit().putString(rememberedUniqueAppIdKey, testId).apply()
assertEquals(testId, authDiskSource.uniqueAppId)
}
@Test
fun `rememberedEmailAddress should pull from and update SharedPreferences`() {
val rememberedEmailKey = "bwPreferencesStorage:rememberedEmail"

View file

@ -8,6 +8,8 @@ import kotlinx.coroutines.flow.onSubscription
import org.junit.Assert.assertEquals
class FakeAuthDiskSource : AuthDiskSource {
override val uniqueAppId: String = "testUniqueAppId"
override var rememberedEmailAddress: String? = null
override var userState: UserStateJson? = null

View file

@ -41,6 +41,7 @@ class IdentityServiceTest : BaseServiceTest() {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
assertEquals(Result.success(LOGIN_SUCCESS), result)
}
@ -52,6 +53,7 @@ class IdentityServiceTest : BaseServiceTest() {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
assertTrue(result.isFailure)
}
@ -63,6 +65,7 @@ class IdentityServiceTest : BaseServiceTest() {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
assertEquals(Result.success(CAPTCHA_BODY), result)
}
@ -74,6 +77,7 @@ class IdentityServiceTest : BaseServiceTest() {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
assertEquals(Result.success(INVALID_LOGIN), result)
}
@ -94,6 +98,7 @@ class IdentityServiceTest : BaseServiceTest() {
}
companion object {
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val REFRESH_TOKEN = "refreshToken"
private const val EMAIL = "email"
private const val PASSWORD_HASH = "passwordHash"

View file

@ -320,6 +320,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
.returns(Result.failure(RuntimeException()))
@ -332,6 +333,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
}
@ -346,6 +348,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
} returns Result.success(
GetTokenResponseJson.Invalid(
@ -364,6 +367,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
}
@ -381,6 +385,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
.returns(Result.success(successResponse))
@ -419,6 +424,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
vaultRepository.unlockVault(
userId = USER_ID_1,
@ -441,6 +447,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
.returns(Result.success(GetTokenResponseJson.CaptchaRequired(CAPTCHA_KEY)))
@ -453,6 +460,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
}
@ -807,6 +815,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
} returns Result.success(successResponse)
coEvery {
@ -870,6 +879,7 @@ class AuthRepositoryTest {
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
} returns Result.success(successResponse)
coEvery {
@ -975,6 +985,7 @@ class AuthRepositoryTest {
"com.x8bit.bitwarden.data.auth.repository.util.GetTokenResponseExtensionsKt"
private const val REFRESH_TOKEN_RESPONSE_EXTENSIONS_PATH =
"com.x8bit.bitwarden.data.auth.repository.util.RefreshTokenResponseExtensionsKt"
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val EMAIL = "test@bitwarden.com"
private const val EMAIL_2 = "test2@bitwarden.com"
private const val PASSWORD = "password"