From 2d4427a7cf10f5536a21a8f479fe9892824e00f6 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Mon, 8 Jan 2024 21:52:26 -0600 Subject: [PATCH] Add EncryptedSharedPreferences and BaseEncryptedDiskSource (#543) --- README.md | 5 +++ app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 +- .../datasource/disk/AuthDiskSourceImpl.kt | 8 +++-- .../auth/datasource/disk/di/AuthDiskModule.kt | 6 +++- .../datasource/di/EncryptedPreferences.kt | 11 +++++++ .../datasource/di/PreferenceModule.kt | 22 ++++++++++++- .../datasource/di/UnencryptedPreferences.kt | 11 +++++++ .../disk/BaseEncryptedDiskSource.kt | 31 ++++++++++++++++++ .../datasource/disk/di/PlatformDiskModule.kt | 5 +-- .../datasource/disk/di/GeneratorDiskModule.kt | 3 +- app/src/main/res/xml/backup_rules.xml | 15 +++++++++ .../main/res/xml/data_extraction_rules.xml | 32 +++++++++++++++++++ .../datasource/disk/AuthDiskSourceTest.kt | 2 ++ gradle/libs.versions.toml | 2 ++ 15 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/EncryptedPreferences.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/UnencryptedPreferences.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseEncryptedDiskSource.kt diff --git a/README.md b/README.md index 4835ca5b6..c5e92a983 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 09b0dd997..e9565d9d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 167a02750..183fe41f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ ?>>() diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt index 1fce3bba7..39c6fc666 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt @@ -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, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/EncryptedPreferences.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/EncryptedPreferences.kt new file mode 100644 index 000000000..08b64d473 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/EncryptedPreferences.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/PreferenceModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/PreferenceModule.kt index b0677f162..cd69445f5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/PreferenceModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/PreferenceModule.kt @@ -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, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/UnencryptedPreferences.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/UnencryptedPreferences.kt new file mode 100644 index 000000000..277b18ef4 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/di/UnencryptedPreferences.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseEncryptedDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseEncryptedDiskSource.kt new file mode 100644 index 000000000..87e1d37cb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseEncryptedDiskSource.kt @@ -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" + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt index 4c38bd1b6..b901bf716 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/disk/di/GeneratorDiskModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/disk/di/GeneratorDiskModule.kt index abef7dd94..55865c71e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/disk/di/GeneratorDiskModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/disk/di/GeneratorDiskModule.kt @@ -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( diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index a608293f2..cf7893b5b 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -1,3 +1,18 @@ + + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index 288f6904b..b8ddf7c0d 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -1,5 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 39a557f79..b0bc426f3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -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, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 91b489c88..7aeab5445 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }