diff --git a/README.md b/README.md index 3413e51..b9fbe85 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Roboswag позволяет сочетать эти три решения в о ### Работа с RecyclerView RecyclerView - один из самых часто используемых инструментов Android разработчика. Модуль [recyclerview-adapters](/recyclerview-adapters) позволяет сделать работу с RecyclerView более гибкой и делает работу самого элемента быстрее. ### Работа с SharedPreferences -Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. +Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. Для шифрования данных в SharedPreferences можно использовать [encrypted-shared-prefs](/encrypted-shared-prefs) ### Утилиты и extension функции В Roboswag также есть много [утилитарных](/utils) классов и [extension](/kotlin-extensions) функций, которые позволяют писать часто используемый код в одну строку. @@ -62,7 +62,8 @@ gradle.ext.roboswag = [ 'tabbar-navigation', 'base-map', 'yandex-map', - 'google-map' + 'google-map', + 'encrypted-shared-prefs' ] gradle.ext.roboswag.forEach { module -> diff --git a/encrypted-shared-prefs/README.md b/encrypted-shared-prefs/README.md new file mode 100644 index 0000000..481ca7d --- /dev/null +++ b/encrypted-shared-prefs/README.md @@ -0,0 +1,14 @@ +Encrypted shared preferences +============================ + +Модуль с реализацией интерфейса `SharedPreferences`, который дает возможность шифровать содержимое. + +### Пример + +Пример получения экземпляра `TouchinSharedPreferences`. При encrypt = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` + +```kotlin +val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, encrypt = true) +``` + +Важно помнить, что в одном файле `TouchinSharedPreferences` могут храниться только либо полностью зашифрованные данные, либо полностью незашифрованные. Флаг `isEncryption` должен быть в соответствующем положении diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt similarity index 69% rename from encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt rename to encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt index dae6b07..65eb56d 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt @@ -2,11 +2,13 @@ package ru.touchin.roboswag import android.content.Context import android.content.SharedPreferences +import ru.touchin.roboswag.PrefsCryptoUtils.Companion.ENCRYPT_BASE64_STRING_LENGTH +import ru.touchin.roboswag.PrefsCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE -class TouchinSharedPreferences(name: String, context: Context, val isEncryption: Boolean = false) : SharedPreferences { +class CipherSharedPreferences(name: String, context: Context, val encrypt: Boolean = false) : SharedPreferences { private val currentPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) - private val cryptoUtils = TouchinSharedPreferencesCryptoUtils(context) + private val cryptoUtils = PrefsCryptoUtils(context) override fun contains(key: String?) = currentPreferences.contains(key) @@ -19,7 +21,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun getInt(key: String?, defaultValue: Int) = get(key, defaultValue) override fun getAll(): MutableMap { - return if (isEncryption) { + return if (encrypt) { currentPreferences.all.mapValues { it.value.toString().decrypt() }.toMutableMap() } else { currentPreferences.all.mapValues { it.value.toString() }.toMutableMap() @@ -35,7 +37,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun getString(key: String?, defaultValue: String?): String = get(key, defaultValue ?: "") override fun getStringSet(key: String?, set: MutableSet?): MutableSet? { - return if (isEncryption) { + return if (encrypt) { val value = currentPreferences.getStringSet(key, set) if (value == set) { set @@ -53,18 +55,22 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: private fun get(key: String?, defaultValue: T): T { if (!currentPreferences.contains(key)) return defaultValue - val value = currentPreferences.getString(key, "")?.decrypt() + val resultValue = currentPreferences.getString(key, "") + ?.trim() + ?.chunked(ENCRYPT_BASE64_STRING_LENGTH) + ?.joinToString(separator = "", transform = { it.decrypt() }) + return when (defaultValue) { - is Boolean -> value?.toBoolean() as? T - is Long -> value?.toLong() as? T - is String -> value as? T - is Int -> value?.toInt() as? T - is Float -> value?.toFloat() as? T - else -> value as? T + is Boolean -> resultValue?.toBoolean() as? T + is Long -> resultValue?.toLong() as? T + is String -> resultValue as? T + is Int -> resultValue?.toInt() as? T + is Float -> resultValue?.toFloat() as? T + else -> resultValue as? T } ?: defaultValue } - private fun String.decrypt() = if (isEncryption) cryptoUtils.decrypt(this) else this + private fun String.decrypt() = if (encrypt) cryptoUtils.decrypt(this) else this inner class TouchinEditor : SharedPreferences.Editor { @@ -79,7 +85,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun putBoolean(key: String?, value: Boolean) = put(key, value) override fun putStringSet(key: String?, value: MutableSet?): SharedPreferences.Editor { - return if (isEncryption) { + return if (encrypt) { currentPreferences.edit().putStringSet(key, value?.map { it.encrypt() }?.toMutableSet()) } else { currentPreferences.edit().putStringSet(key, value) @@ -94,9 +100,13 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun putString(key: String?, value: String?) = put(key, value) - private fun put(key: String?, value: T) = currentPreferences.edit().putString(key, value.toString().encrypt()) + private fun put(key: String?, value: T): SharedPreferences.Editor { + val resultValue = value?.toString()?.chunked(ENCRYPT_BLOCK_SIZE)?.joinToString(separator = "", transform = { it.encrypt() }) - private fun String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this + return currentPreferences.edit().putString(key, resultValue) + } + + private fun String.encrypt() = if (encrypt) cryptoUtils.encrypt(this) else this } diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt index 4e4fc87..bcab65b 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt @@ -2,7 +2,7 @@ package ru.touchin.roboswag import android.content.SharedPreferences -fun TouchinSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { +fun CipherSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { if (!from.contains(key)) return this edit().putString(key, from.getString(key, "") ?: "").apply() from.edit().remove(key).apply() diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt similarity index 87% rename from encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt rename to encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt index 2cc1b82..3a3c888 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt @@ -18,14 +18,24 @@ import javax.security.auth.x500.X500Principal // https://proandroiddev.com/secure-data-in-android-encryption-in-android-part-2-991a89e55a23 @Suppress("detekt.TooGenericExceptionCaught") -class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { +class PrefsCryptoUtils constructor(val context: Context) { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val STORAGE_KEY = "STORAGE_KEY" private const val KEY_ALGORITHM_RSA = "RSA" private const val TRANSFORMATION_ASYMMETRIC = "RSA/ECB/PKCS1Padding" + private const val CIPHER_STRING_SIZE_BYTES = 256 + private const val BASE_64_PADDING = 2 + private const val STORAGE_KEY = "STORAGE_KEY" + + //https://stackoverflow.com/questions/13378815/base64-length-calculation + private const val DECRYPTED_BYTES_COUNT = 3 + private const val ENCRYPTED_BYTES_COUNT = 4 + private const val BASE64_DIVIDER_COUNT = 5 + const val ENCRYPT_BASE64_STRING_LENGTH = + (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * ENCRYPTED_BYTES_COUNT / DECRYPTED_BYTES_COUNT + BASE64_DIVIDER_COUNT + const val ENCRYPT_BLOCK_SIZE = 128 private fun getAndroidKeystore(): KeyStore? = try { KeyStore.getInstance(ANDROID_KEY_STORE).also { it.load(null) }