mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 01:16:02 +03:00
BIT-1430: Add migration from SecureStorage (#728)
This commit is contained in:
parent
17eb3e2e0b
commit
82d06f56b9
10 changed files with 559 additions and 1 deletions
|
@ -4,8 +4,8 @@ import android.content.SharedPreferences
|
|||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource.Companion.ENCRYPTED_BASE_KEY
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -35,12 +35,20 @@ private const val ORGANIZATION_KEYS_KEY = "$BASE_KEY:encOrgKeys"
|
|||
class AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences: SharedPreferences,
|
||||
sharedPreferences: SharedPreferences,
|
||||
legacySecureStorageMigrator: LegacySecureStorageMigrator,
|
||||
private val json: Json,
|
||||
) : BaseEncryptedDiskSource(
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
sharedPreferences = sharedPreferences,
|
||||
),
|
||||
AuthDiskSource {
|
||||
|
||||
init {
|
||||
// We must migrate if necessary before any of the migrated values would be initialized
|
||||
// and accessed.
|
||||
legacySecureStorageMigrator.migrateIfNecessary()
|
||||
}
|
||||
|
||||
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
|
||||
private val mutableOrganizationsFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
|||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.EncryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -24,11 +25,13 @@ object AuthDiskModule {
|
|||
fun provideAuthDiskSource(
|
||||
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
|
||||
@UnencryptedPreferences sharedPreferences: SharedPreferences,
|
||||
legacySecureStorageMigrator: LegacySecureStorageMigrator,
|
||||
json: Json,
|
||||
): AuthDiskSource =
|
||||
AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
sharedPreferences = sharedPreferences,
|
||||
legacySecureStorageMigrator = legacySecureStorageMigrator,
|
||||
json = json,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.EncryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
|
||||
|
@ -8,9 +10,14 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
|||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorage
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigratorImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.inject.Singleton
|
||||
|
@ -33,6 +40,26 @@ object PlatformDiskModule {
|
|||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLegacySecureStorage(
|
||||
@ApplicationContext context: Context,
|
||||
): LegacySecureStorage =
|
||||
LegacySecureStorageImpl(
|
||||
context = context,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLegacySecureStorageMigrator(
|
||||
legacySecureStorage: LegacySecureStorage,
|
||||
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
|
||||
): LegacySecureStorageMigrator =
|
||||
LegacySecureStorageMigratorImpl(
|
||||
legacySecureStorage = legacySecureStorage,
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePushDiskSource(
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.legacy
|
||||
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
|
||||
/**
|
||||
* Represents a legacy storage system that exists only to migrate data to
|
||||
* [EncryptedSharedPreferences]. Because no new data will be stored here, only the ability to
|
||||
* retrieve and clear data is provided.
|
||||
*/
|
||||
interface LegacySecureStorage {
|
||||
/**
|
||||
* Returns the data for the given [key], or `null` if no data for that key is present or if
|
||||
* decryption has failed.
|
||||
*/
|
||||
fun get(key: String): String?
|
||||
|
||||
/**
|
||||
* Returns all of the raw keys stored. In some cases these will be hashed versions of the keys
|
||||
* passed to [get].
|
||||
*/
|
||||
fun getRawKeys(): Set<String>
|
||||
|
||||
/**
|
||||
* Removes the data for the given [key].
|
||||
*/
|
||||
fun remove(key: String)
|
||||
|
||||
/**
|
||||
* Removes all data stored.
|
||||
*/
|
||||
fun removeAll()
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.security.KeyPairGeneratorSpec
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import androidx.core.content.edit
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import java.math.BigInteger
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.Key
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.PrivateKey
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.crypto.AEADBadTagException
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
/**
|
||||
* Primary implementation of [LegacySecureStorage].
|
||||
*
|
||||
* Apart from removing the ability to store new data (rather than just retrieve it) this is adapted
|
||||
* with minimal modifications from the [Xamarin.Essentials.SecureStorage source code](https://github.com/xamarin/Essentials/blob/main/Xamarin.Essentials/SecureStorage/SecureStorage.android.cs)
|
||||
*/
|
||||
@Suppress(
|
||||
"NestedBlockDepth",
|
||||
"TooGenericExceptionCaught",
|
||||
"MagicNumber",
|
||||
)
|
||||
@OmitFromCoverage
|
||||
class LegacySecureStorageImpl(
|
||||
private val context: Context,
|
||||
) : LegacySecureStorage {
|
||||
private val alias = "${context.packageName}.xamarinessentials"
|
||||
private val sharedPreferences = context.getSharedPreferences(alias, Context.MODE_PRIVATE)
|
||||
private val locker = Any()
|
||||
|
||||
private val legacyKeyHashFallback: Boolean = true
|
||||
|
||||
override fun get(key: String): String? {
|
||||
if (key.isBlank()) return null
|
||||
return platformGetAsync(key)
|
||||
}
|
||||
|
||||
override fun getRawKeys(): Set<String> =
|
||||
sharedPreferences.all.keys
|
||||
|
||||
override fun remove(key: String) {
|
||||
platformRemove(key)
|
||||
}
|
||||
|
||||
override fun removeAll() {
|
||||
platformRemoveAll()
|
||||
}
|
||||
|
||||
private fun platformGetAsync(key: String): String? {
|
||||
|
||||
var encStr: String? = null
|
||||
var foundLegacyValue = false
|
||||
|
||||
if (legacyKeyHashFallback) {
|
||||
if (!sharedPreferences.all.containsKey(key)) {
|
||||
val md5Key = md5Hash(key)
|
||||
if (sharedPreferences.all.containsKey(md5Key)) {
|
||||
encStr = sharedPreferences.getString(md5Key, null)
|
||||
sharedPreferences.edit { putString(key, encStr) }
|
||||
foundLegacyValue = true
|
||||
|
||||
try {
|
||||
sharedPreferences.edit { remove(md5Key) }
|
||||
} catch (e: Exception) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundLegacyValue) {
|
||||
encStr = sharedPreferences.getString(key, null)
|
||||
}
|
||||
|
||||
var decryptedData: String? = null
|
||||
if (!encStr.isNullOrBlank()) {
|
||||
try {
|
||||
val encData = Base64.decode(encStr, Base64.DEFAULT)
|
||||
synchronized(locker) {
|
||||
val ks = AndroidKeyStore(
|
||||
legacySecureStorage = this,
|
||||
sharedPreferences = sharedPreferences,
|
||||
context = context,
|
||||
keystoreAlias = alias,
|
||||
alwaysUseAsymmetricKeyStorage = false,
|
||||
)
|
||||
decryptedData = ks.decrypt(encData)
|
||||
}
|
||||
} catch (e: AEADBadTagException) {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedData
|
||||
}
|
||||
|
||||
private fun platformRemove(key: String): Boolean {
|
||||
sharedPreferences.edit { remove(key) }
|
||||
checkForAndRemoveLegacyKey(key)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun checkForAndRemoveLegacyKey(key: String) {
|
||||
if (legacyKeyHashFallback) {
|
||||
val md5Key = md5Hash(key)
|
||||
if (sharedPreferences.all.containsKey(md5Key)) {
|
||||
try {
|
||||
sharedPreferences.edit { remove(md5Key) }
|
||||
} catch (e: Exception) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun platformRemoveAll() {
|
||||
sharedPreferences.edit { clear() }
|
||||
}
|
||||
|
||||
private fun md5Hash(input: String): String {
|
||||
val hash = StringBuilder()
|
||||
val md5provider = MessageDigest.getInstance("MD5")
|
||||
val bytes = md5provider.digest(input.toByteArray())
|
||||
|
||||
for (i in bytes.indices) {
|
||||
hash.append(String.format("%02x", bytes[i]))
|
||||
}
|
||||
|
||||
return hash.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private class AndroidKeyStore(
|
||||
private val legacySecureStorage: LegacySecureStorage,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val context: Context,
|
||||
private val keystoreAlias: String,
|
||||
alwaysUseAsymmetricKeyStorage: Boolean,
|
||||
) {
|
||||
private val alwaysUseAsymmetricKey: Boolean = alwaysUseAsymmetricKeyStorage
|
||||
private val alias: String = keystoreAlias
|
||||
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
private val useSymmetricPreferenceKey: String = "essentials_use_symmetric"
|
||||
|
||||
private val prefsMasterKey = "SecureStorageKey"
|
||||
private val initializationVectorLen = 12; // Android supports an IV of 12 for AES/GCM
|
||||
|
||||
init {
|
||||
keyStore.load(null)
|
||||
}
|
||||
|
||||
private fun getKey(): SecretKey {
|
||||
val useSymmetric = sharedPreferences.getBoolean(
|
||||
useSymmetricPreferenceKey,
|
||||
true,
|
||||
)
|
||||
|
||||
return if (useSymmetric && !alwaysUseAsymmetricKey) {
|
||||
getSymmetricKey()
|
||||
} else {
|
||||
val keyPair = getAsymmetricKeyPair()
|
||||
|
||||
val existingKeyStr = sharedPreferences.getString(prefsMasterKey, null)
|
||||
|
||||
if (!existingKeyStr.isNullOrBlank()) {
|
||||
try {
|
||||
val wrappedKey = Base64.decode(existingKeyStr, Base64.DEFAULT)
|
||||
val unwrappedKey = unwrapKey(wrappedKey, keyPair.private)
|
||||
return unwrappedKey as SecretKey
|
||||
} catch (ikEx: InvalidKeyException) {
|
||||
// no-op
|
||||
} catch (ibsEx: IllegalBlockSizeException) {
|
||||
// no-op
|
||||
} catch (paddingEx: BadPaddingException) {
|
||||
// no-op
|
||||
}
|
||||
legacySecureStorage.removeAll()
|
||||
}
|
||||
|
||||
val keyGenerator = KeyGenerator.getInstance("AES")
|
||||
val defSymmetricKey = keyGenerator.generateKey()
|
||||
|
||||
val newWrappedKey = wrapKey(defSymmetricKey, keyPair.public)
|
||||
|
||||
sharedPreferences.edit {
|
||||
putString(
|
||||
prefsMasterKey,
|
||||
Base64.encodeToString(newWrappedKey, Base64.DEFAULT),
|
||||
)
|
||||
}
|
||||
|
||||
defSymmetricKey
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSymmetricKey(): SecretKey {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(useSymmetricPreferenceKey, true)
|
||||
}
|
||||
|
||||
val existingKey = keyStore.getKey(alias, null)
|
||||
|
||||
return if (existingKey != null) {
|
||||
existingKey as SecretKey
|
||||
} else {
|
||||
val keyGenerator =
|
||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
||||
val builder = KeyGenParameterSpec
|
||||
.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setRandomizedEncryptionRequired(false)
|
||||
|
||||
keyGenerator.init(builder.build())
|
||||
|
||||
keyGenerator.generateKey()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAsymmetricKeyPair(): KeyPair {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(useSymmetricPreferenceKey, false)
|
||||
}
|
||||
|
||||
val asymmetricAlias = "$alias.asymmetric"
|
||||
val privateKey = keyStore.getKey(asymmetricAlias, null) as PrivateKey?
|
||||
val publicKey = keyStore.getCertificate(asymmetricAlias)?.publicKey
|
||||
|
||||
return if (privateKey != null && publicKey != null) {
|
||||
KeyPair(publicKey, privateKey)
|
||||
} else {
|
||||
val originalLocale = Locale.getDefault()
|
||||
try {
|
||||
Locale.setDefault(Locale.ENGLISH)
|
||||
val generator =
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
|
||||
val end = ZonedDateTime.now(ZoneOffset.UTC).plusYears(20)
|
||||
val startDate = Date()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val endDate = Date(end.year, end.month.value, end.dayOfMonth)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val builder = KeyPairGeneratorSpec.Builder(context)
|
||||
.setAlias(asymmetricAlias)
|
||||
.setSerialNumber(BigInteger.ONE)
|
||||
.setSubject(X500Principal("CN=$asymmetricAlias CA Certificate"))
|
||||
.setStartDate(startDate)
|
||||
.setEndDate(endDate)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
generator.initialize(builder.build())
|
||||
generator.generateKeyPair()
|
||||
} finally {
|
||||
Locale.setDefault(originalLocale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wrapKey(keyToWrap: Key, withKey: Key): ByteArray {
|
||||
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
|
||||
cipher.init(Cipher.WRAP_MODE, withKey)
|
||||
return cipher.wrap(keyToWrap)
|
||||
}
|
||||
|
||||
private fun unwrapKey(wrappedData: ByteArray, withKey: Key): Key {
|
||||
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
|
||||
cipher.init(Cipher.UNWRAP_MODE, withKey)
|
||||
return cipher.unwrap(
|
||||
wrappedData,
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
Cipher.SECRET_KEY,
|
||||
) as SecretKey
|
||||
}
|
||||
|
||||
@SuppressLint("GetInstance")
|
||||
fun decrypt(data: ByteArray): String? {
|
||||
if (data.size < initializationVectorLen) {
|
||||
return null
|
||||
}
|
||||
|
||||
val key = getKey()
|
||||
|
||||
val iv = ByteArray(initializationVectorLen)
|
||||
System.arraycopy(data, 0, iv, 0, initializationVectorLen)
|
||||
|
||||
val cipher = try {
|
||||
Cipher.getInstance("AES/GCM/NoPadding")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Cipher.getInstance("AES/ECB/PKCS5Padding") // Fallback for old devices
|
||||
}
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
|
||||
} catch (e: InvalidAlgorithmParameterException) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||
}
|
||||
|
||||
val decryptedData =
|
||||
cipher.doFinal(data, initializationVectorLen, data.size - initializationVectorLen)
|
||||
|
||||
return String(decryptedData, StandardCharsets.UTF_8)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.legacy
|
||||
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
|
||||
/**
|
||||
* Provides the ability to migrate from a legacy "secure storage" system to
|
||||
* [EncryptedSharedPreferences].
|
||||
*/
|
||||
interface LegacySecureStorageMigrator {
|
||||
|
||||
/**
|
||||
* Migrates any data from the legacy "secure storage" system to [EncryptedSharedPreferences].
|
||||
* After migration, data will be removed from the legacy system.
|
||||
*/
|
||||
fun migrateIfNecessary()
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.legacy
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
|
||||
|
||||
/**
|
||||
* Primary implementation of [LegacySecureStorageMigrator].
|
||||
*/
|
||||
class LegacySecureStorageMigratorImpl(
|
||||
private val legacySecureStorage: LegacySecureStorage,
|
||||
private val encryptedSharedPreferences: SharedPreferences,
|
||||
) : LegacySecureStorageMigrator {
|
||||
|
||||
override fun migrateIfNecessary() {
|
||||
// If there are no remaining keys, there is no migration to perform.
|
||||
val keys = legacySecureStorage.getRawKeys()
|
||||
if (keys.isEmpty()) return
|
||||
|
||||
// For now we are primarily concerned with keys that have not been hashed before storage,
|
||||
// which will all start with "bwSecureStorage". Hashing only occurred on devices with
|
||||
// SDK <23.
|
||||
val plaintextKeys = keys.filter {
|
||||
it.startsWith(BaseEncryptedDiskSource.ENCRYPTED_BASE_KEY)
|
||||
}
|
||||
|
||||
plaintextKeys.forEach { unhashedKey ->
|
||||
val decryptedValue = legacySecureStorage.get(unhashedKey)
|
||||
encryptedSharedPreferences.edit {
|
||||
putString(unhashedKey, decryptedValue)
|
||||
}
|
||||
legacySecureStorage.remove(unhashedKey)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,8 +11,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDe
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
|
@ -25,15 +30,24 @@ import org.junit.jupiter.api.Test
|
|||
class AuthDiskSourceTest {
|
||||
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
private val legacySecureStorageMigrator = mockk<LegacySecureStorageMigrator>() {
|
||||
every { migrateIfNecessary() } just runs
|
||||
}
|
||||
|
||||
private val json = PlatformNetworkModule.providesJson()
|
||||
|
||||
private val authDiskSource = AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
legacySecureStorageMigrator = legacySecureStorageMigrator,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `initialization should kick off a legacy migration if necessary`() {
|
||||
every { legacySecureStorageMigrator.migrateIfNecessary() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uniqueAppId should generate a new ID and update SharedPreferences if none exists`() {
|
||||
val rememberedUniqueAppIdKey = "bwPreferencesStorage:appId"
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.disk.legacy
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorage
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigratorImpl
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LegacySecureStorageMigratorTest {
|
||||
|
||||
private val fakeLegacySecureStorage = FakeLegacySecureStorage()
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
private val legacySecureStorageMigrator = LegacySecureStorageMigratorImpl(
|
||||
legacySecureStorage = fakeLegacySecureStorage,
|
||||
encryptedSharedPreferences = fakeSharedPreferences,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `migrateIfNecessary when there are no keys left should do nothing`() {
|
||||
assertTrue(fakeSharedPreferences.all.keys.isEmpty())
|
||||
|
||||
legacySecureStorageMigrator.migrateIfNecessary()
|
||||
|
||||
assertTrue(fakeSharedPreferences.all.keys.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrateIfNecessary when the keys do not start with bwSecureStorage should do nothing`() {
|
||||
assertTrue(fakeSharedPreferences.all.keys.isEmpty())
|
||||
|
||||
fakeLegacySecureStorage.put(
|
||||
key = "hashedKey",
|
||||
value = "value",
|
||||
)
|
||||
legacySecureStorageMigrator.migrateIfNecessary()
|
||||
|
||||
assertTrue(fakeSharedPreferences.all.keys.isEmpty())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `migrateIfNecessary when the keys start with bwSecureStorage should migrate the data and remove it from the legacy storage`() {
|
||||
assertTrue(fakeSharedPreferences.all.keys.isEmpty())
|
||||
|
||||
val userId = "userId"
|
||||
val key = "bwSecureStorage:userKeyAutoUnlock_$userId"
|
||||
val value = "mockUserAutoUnlockKey"
|
||||
fakeLegacySecureStorage.put(
|
||||
key = key,
|
||||
value = value,
|
||||
)
|
||||
legacySecureStorageMigrator.migrateIfNecessary()
|
||||
|
||||
assertFalse(fakeSharedPreferences.all.keys.isEmpty())
|
||||
assertEquals(
|
||||
value,
|
||||
fakeSharedPreferences.getString(
|
||||
key = key,
|
||||
defaultValue = null,
|
||||
),
|
||||
)
|
||||
assertNull(
|
||||
fakeLegacySecureStorage.get(
|
||||
key = key,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeLegacySecureStorage : LegacySecureStorage {
|
||||
private val dataMap = mutableMapOf<String, String>()
|
||||
|
||||
override fun get(key: String): String? =
|
||||
dataMap[key]
|
||||
|
||||
override fun getRawKeys(): Set<String> =
|
||||
dataMap.keys
|
||||
|
||||
override fun remove(key: String) {
|
||||
dataMap.remove(key)
|
||||
}
|
||||
|
||||
override fun removeAll() {
|
||||
dataMap.clear()
|
||||
}
|
||||
|
||||
fun put(key: String, value: String) {
|
||||
dataMap[key] = value
|
||||
}
|
||||
}
|
BIN
keystores/xamarin-debug.keystore
Normal file
BIN
keystores/xamarin-debug.keystore
Normal file
Binary file not shown.
Loading…
Reference in a new issue