Add EncryptedSharedPreferences and BaseEncryptedDiskSource (#543)

This commit is contained in:
Brian Yencho 2024-01-08 21:52:26 -06:00 committed by Álison Fernandes
parent 14d686af76
commit 2d4427a7cf
15 changed files with 148 additions and 8 deletions

View file

@ -72,6 +72,11 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: Display and capture images for barcode scanning.
- License: Apache 2.0
- **AndroidX Security**
- https://developer.android.com/jetpack/androidx/releases/security
- Purpose: Safely manage keys and encrypt files and sharedpreferences.
- License: Apache 2.0
- **Core SplashScreen**
- https://developer.android.com/jetpack/androidx/releases/core
- Purpose: Backwards compatible SplashScreen API implementation.

View file

@ -121,6 +121,7 @@ dependencies {
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.splashscreen)
implementation(libs.bitwarden.sdk)
implementation(libs.bumptech.glide)

View file

@ -11,7 +11,7 @@
<application
android:name=".BitwardenApplication"
android:allowBackup="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"

View file

@ -2,8 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource
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.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
@ -26,9 +26,13 @@ private const val ORGANIZATION_KEYS_KEY = "$BASE_KEY:encOrgKeys"
*/
@Suppress("TooManyFunctions")
class AuthDiskSourceImpl(
encryptedSharedPreferences: SharedPreferences,
sharedPreferences: SharedPreferences,
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
) : BaseEncryptedDiskSource(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
),
AuthDiskSource {
private val mutableOrganizationsFlowMap =
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()

View file

@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.di
import android.content.SharedPreferences
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 dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,10 +22,12 @@ object AuthDiskModule {
@Provides
@Singleton
fun provideAuthDiskSource(
sharedPreferences: SharedPreferences,
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): AuthDiskSource =
AuthDiskSourceImpl(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
json = json,
)

View file

@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.platform.datasource.di
import android.content.SharedPreferences
import javax.inject.Qualifier
/**
* Used to denote an instance of [SharedPreferences] that encrypts its data.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class EncryptedPreferences

View file

@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.platform.datasource.di
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -18,11 +20,29 @@ object PreferenceModule {
@Provides
@Singleton
fun provideDefaultSharedPreferences(
@UnencryptedPreferences
fun provideUnencryptedSharedPreferences(
application: Application,
): SharedPreferences =
application.getSharedPreferences(
"${application.packageName}_preferences",
Context.MODE_PRIVATE,
)
@Provides
@Singleton
@EncryptedPreferences
fun provideEncryptedSharedPreferences(
application: Application,
): SharedPreferences =
EncryptedSharedPreferences
.create(
application,
"${application.packageName}_encrypted_preferences",
MasterKey.Builder(application)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
}

View file

@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.platform.datasource.di
import android.content.SharedPreferences
import javax.inject.Qualifier
/**
* Used to denote an instance of [SharedPreferences] that does not encrypt its data.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UnencryptedPreferences

View file

@ -0,0 +1,31 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
/**
* Base class for simplifying interactions with [SharedPreferences] and
* [EncryptedSharedPreferences].
*/
@Suppress("UnnecessaryAbstractClass")
abstract class BaseEncryptedDiskSource(
sharedPreferences: SharedPreferences,
private val encryptedSharedPreferences: SharedPreferences,
) : BaseDiskSource(
sharedPreferences = sharedPreferences,
) {
protected fun getEncryptedString(
key: String,
default: String? = null,
): String? = encryptedSharedPreferences.getString(key, default)
protected fun putEncryptedString(
key: String,
value: String?,
): Unit = encryptedSharedPreferences.edit { putString(key, value) }
companion object {
const val ENCRYPTED_BASE_KEY: String = "bwSecureStorage"
}
}

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.di
import android.content.SharedPreferences
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
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
@ -22,7 +23,7 @@ object PlatformDiskModule {
@Provides
@Singleton
fun provideEnvironmentDiskSource(
sharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): EnvironmentDiskSource =
EnvironmentDiskSourceImpl(
@ -33,7 +34,7 @@ object PlatformDiskModule {
@Provides
@Singleton
fun provideSettingsDiskSource(
sharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
): SettingsDiskSource =
SettingsDiskSourceImpl(
sharedPreferences = sharedPreferences,

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.disk.di
import android.app.Application
import android.content.SharedPreferences
import androidx.room.Room
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSourceImpl
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
@ -26,7 +27,7 @@ object GeneratorDiskModule {
@Provides
@Singleton
fun provideGeneratorDiskSource(
sharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): GeneratorDiskSource =
GeneratorDiskSourceImpl(

View file

@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
<exclude
domain="database"
path="." />
<exclude
domain="external"
path="." />
<exclude
domain="root"
path="." />
</full-backup-content>

View file

@ -1,5 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
<exclude
domain="database"
path="." />
<exclude
domain="external"
path="." />
<exclude
domain="root"
path="." />
</cloud-backup>
<device-transfer>
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
<exclude
domain="database"
path="." />
<exclude
domain="external"
path="." />
<exclude
domain="root"
path="." />
</device-transfer>
</data-extraction-rules>

View file

@ -20,11 +20,13 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class AuthDiskSourceTest {
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
private val fakeSharedPreferences = FakeSharedPreferences()
private val json = PlatformNetworkModule.providesJson()
private val authDiskSource = AuthDiskSourceImpl(
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
sharedPreferences = fakeSharedPreferences,
json = json,
)

View file

@ -21,6 +21,7 @@ androidxHiltNavigationCompose = "1.1.0"
androidxLifecycle = "2.6.2"
androidxNavigation = "2.7.6"
androidxRoom = "2.6.1"
androidXSecurityCrypto = "1.1.0-alpha06"
androidxSplash = "1.1.0-alpha02"
# Once the app and SDK reach a critical point of completeness we should begin fixing the version
# here (BIT-311).
@ -74,6 +75,7 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidXSecurityCrypto" }
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" }
bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" }
bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" }