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. - Purpose: Display and capture images for barcode scanning.
- License: Apache 2.0 - 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** - **Core SplashScreen**
- https://developer.android.com/jetpack/androidx/releases/core - https://developer.android.com/jetpack/androidx/releases/core
- Purpose: Backwards compatible SplashScreen API implementation. - Purpose: Backwards compatible SplashScreen API implementation.

View file

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

View file

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

View file

@ -2,8 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
import android.content.SharedPreferences import android.content.SharedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson 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.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.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -26,9 +26,13 @@ private const val ORGANIZATION_KEYS_KEY = "$BASE_KEY:encOrgKeys"
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class AuthDiskSourceImpl( class AuthDiskSourceImpl(
encryptedSharedPreferences: SharedPreferences,
sharedPreferences: SharedPreferences, sharedPreferences: SharedPreferences,
private val json: Json, private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences), ) : BaseEncryptedDiskSource(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
),
AuthDiskSource { AuthDiskSource {
private val mutableOrganizationsFlowMap = private val mutableOrganizationsFlowMap =
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>() 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 android.content.SharedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSourceImpl 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.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -20,10 +22,12 @@ object AuthDiskModule {
@Provides @Provides
@Singleton @Singleton
fun provideAuthDiskSource( fun provideAuthDiskSource(
sharedPreferences: SharedPreferences, @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json, json: Json,
): AuthDiskSource = ): AuthDiskSource =
AuthDiskSourceImpl( AuthDiskSourceImpl(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences, sharedPreferences = sharedPreferences,
json = json, 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.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -18,11 +20,29 @@ object PreferenceModule {
@Provides @Provides
@Singleton @Singleton
fun provideDefaultSharedPreferences( @UnencryptedPreferences
fun provideUnencryptedSharedPreferences(
application: Application, application: Application,
): SharedPreferences = ): SharedPreferences =
application.getSharedPreferences( application.getSharedPreferences(
"${application.packageName}_preferences", "${application.packageName}_preferences",
Context.MODE_PRIVATE, 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 package com.x8bit.bitwarden.data.platform.datasource.disk.di
import android.content.SharedPreferences 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.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
@ -22,7 +23,7 @@ object PlatformDiskModule {
@Provides @Provides
@Singleton @Singleton
fun provideEnvironmentDiskSource( fun provideEnvironmentDiskSource(
sharedPreferences: SharedPreferences, @UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json, json: Json,
): EnvironmentDiskSource = ): EnvironmentDiskSource =
EnvironmentDiskSourceImpl( EnvironmentDiskSourceImpl(
@ -33,7 +34,7 @@ object PlatformDiskModule {
@Provides @Provides
@Singleton @Singleton
fun provideSettingsDiskSource( fun provideSettingsDiskSource(
sharedPreferences: SharedPreferences, @UnencryptedPreferences sharedPreferences: SharedPreferences,
): SettingsDiskSource = ): SettingsDiskSource =
SettingsDiskSourceImpl( SettingsDiskSourceImpl(
sharedPreferences = sharedPreferences, sharedPreferences = sharedPreferences,

View file

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

View file

@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<full-backup-content> <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> </full-backup-content>

View file

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

View file

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

View file

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