diff --git a/encrypted-shared-prefs/.gitignore b/encrypted-shared-prefs/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/encrypted-shared-prefs/.gitignore @@ -0,0 +1 @@ +/build diff --git a/encrypted-shared-prefs/build.gradle b/encrypted-shared-prefs/build.gradle new file mode 100644 index 0000000..085c337 --- /dev/null +++ b/encrypted-shared-prefs/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 21 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "androidx.core:core:$versions.androidx" + implementation "androidx.annotation:annotation:$versions.androidx" + implementation "androidx.appcompat:appcompat:$versions.appcompat" + +} diff --git a/encrypted-shared-prefs/src/main/AndroidManifest.xml b/encrypted-shared-prefs/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a068e5f --- /dev/null +++ b/encrypted-shared-prefs/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + 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 new file mode 100644 index 0000000..a4305ed --- /dev/null +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt @@ -0,0 +1,9 @@ +package ru.touchin.roboswag + +import android.content.SharedPreferences + +fun TouchinSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { + if (!from.contains(key)) return this + edit().putString(key, from.getString(key, "") ?: "").apply() + return this +} diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt new file mode 100644 index 0000000..dae6b07 --- /dev/null +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt @@ -0,0 +1,103 @@ +package ru.touchin.roboswag + +import android.content.Context +import android.content.SharedPreferences + +class TouchinSharedPreferences(name: String, context: Context, val isEncryption: Boolean = false) : SharedPreferences { + + private val currentPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) + private val cryptoUtils = TouchinSharedPreferencesCryptoUtils(context) + + override fun contains(key: String?) = currentPreferences.contains(key) + + override fun getBoolean(key: String?, defaultValue: Boolean) = get(key, defaultValue) + + override fun unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener?) { + currentPreferences.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + override fun getInt(key: String?, defaultValue: Int) = get(key, defaultValue) + + override fun getAll(): MutableMap { + return if (isEncryption) { + currentPreferences.all.mapValues { it.value.toString().decrypt() }.toMutableMap() + } else { + currentPreferences.all.mapValues { it.value.toString() }.toMutableMap() + } + } + + override fun edit() = TouchinEditor() + + override fun getLong(key: String?, defaultValue: Long) = get(key, defaultValue) + + override fun getFloat(key: String?, defaultValue: Float) = get(key, defaultValue) + + override fun getString(key: String?, defaultValue: String?): String = get(key, defaultValue ?: "") + + override fun getStringSet(key: String?, set: MutableSet?): MutableSet? { + return if (isEncryption) { + val value = currentPreferences.getStringSet(key, set) + if (value == set) { + set + } else { + value?.map { it.decrypt() }?.toMutableSet() + } + } else { + currentPreferences.getStringSet(key, set) + } + } + + override fun registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener?) { + currentPreferences.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + private fun get(key: String?, defaultValue: T): T { + if (!currentPreferences.contains(key)) return defaultValue + val value = currentPreferences.getString(key, "")?.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 + } ?: defaultValue + } + + private fun String.decrypt() = if (isEncryption) cryptoUtils.decrypt(this) else this + + inner class TouchinEditor : SharedPreferences.Editor { + + override fun clear() = currentPreferences.edit().clear() + + override fun putLong(key: String?, value: Long) = put(key, value) + + override fun putInt(key: String?, value: Int) = put(key, value) + + override fun remove(key: String?) = currentPreferences.edit().remove(key) + + override fun putBoolean(key: String?, value: Boolean) = put(key, value) + + override fun putStringSet(key: String?, value: MutableSet?): SharedPreferences.Editor { + return if (isEncryption) { + currentPreferences.edit().putStringSet(key, value?.map { it.encrypt() }?.toMutableSet()) + } else { + currentPreferences.edit().putStringSet(key, value) + } + } + + override fun commit() = currentPreferences.edit().commit() + + override fun putFloat(key: String?, value: Float) = put(key, value) + + override fun apply() = currentPreferences.edit().apply() + + 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 String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this + + } + +} diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt new file mode 100644 index 0000000..2cc1b82 --- /dev/null +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt @@ -0,0 +1,115 @@ +package ru.touchin.roboswag + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.math.BigInteger +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.util.Calendar +import javax.crypto.Cipher +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) { + + 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 fun getAndroidKeystore(): KeyStore? = try { + KeyStore.getInstance(ANDROID_KEY_STORE).also { it.load(null) } + } catch (exception: Exception) { + null + } + + private fun getAndroidKeyStoreAsymmetricKeyPair(): KeyPair? { + val privateKey = getAndroidKeystore()?.getKey(STORAGE_KEY, null) as PrivateKey? + val publicKey = getAndroidKeystore()?.getCertificate(STORAGE_KEY)?.publicKey + return if (privateKey != null && publicKey != null) { + KeyPair(publicKey, privateKey) + } else { + null + } + } + + private fun createAndroidKeyStoreAsymmetricKey(context: Context): KeyPair { + val generator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, ANDROID_KEY_STORE) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + initGeneratorWithKeyPairGeneratorSpec(generator) + } else { + initGeneratorWithKeyGenParameterSpec(generator, context) + } + + // Generates Key with given spec and saves it to the KeyStore + return generator.generateKeyPair() + } + + private fun initGeneratorWithKeyGenParameterSpec(generator: KeyPairGenerator, context: Context) { + val startDate = Calendar.getInstance() + val endDate = Calendar.getInstance() + endDate.add(Calendar.YEAR, 20) + + val builder = KeyPairGeneratorSpec.Builder(context) + .setAlias(STORAGE_KEY) + .setSerialNumber(BigInteger.ONE) + .setSubject(X500Principal("CN=$STORAGE_KEY CA Certificate")) + .setStartDate(startDate.time) + .setEndDate(endDate.time) + + generator.initialize(builder.build()) + } + + @TargetApi(Build.VERSION_CODES.M) + private fun initGeneratorWithKeyPairGeneratorSpec(generator: KeyPairGenerator) { + val builder = KeyGenParameterSpec.Builder(STORAGE_KEY, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + generator.initialize(builder.build()) + } + + private fun createCipher(): Cipher? = try { + Cipher.getInstance(TRANSFORMATION_ASYMMETRIC) + } catch (exception: Exception) { + null + } + + } + + private val cipher = createCipher() + private val keyPair = getAndroidKeyStoreAsymmetricKeyPair() + ?: createAndroidKeyStoreAsymmetricKey(context) + + // Those methods should not take and return strings, only char[] and those arrays should be cleared right after usage + // See for explanation https://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#PBEEx + + @Synchronized + fun encrypt(data: String): String { + cipher?.init(Cipher.ENCRYPT_MODE, keyPair.public) + val bytes = cipher?.doFinal(data.toByteArray()) + return Base64.encodeToString(bytes, Base64.DEFAULT) + } + + @Synchronized + fun decrypt(data: String?): String { + cipher?.init(Cipher.DECRYPT_MODE, keyPair.private) + if (data.isNullOrBlank()) { + return String() + } + val encryptedData = Base64.decode(data, Base64.DEFAULT) + val decodedData = cipher?.doFinal(encryptedData) + return decodedData?.let { decodedData -> String(decodedData) } ?: "" + } + +} diff --git a/storable/build.gradle b/storable/build.gradle index 2983f29..340a921 100644 --- a/storable/build.gradle +++ b/storable/build.gradle @@ -22,6 +22,4 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" - - implementation "com.github.yandextaxitech:binaryprefs:$versions.binaryprefs" } diff --git a/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java index f541878..1204a53 100644 --- a/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java +++ b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java @@ -21,9 +21,6 @@ package ru.touchin.roboswag.components.utils.storables; import android.content.SharedPreferences; -import com.ironz.binaryprefs.BinaryPreferencesBuilder; -import com.ironz.binaryprefs.Preferences; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -58,24 +55,6 @@ public final class PreferenceUtils { ).build(); } - /** - * Creates {@link Storable} that stores string into {@link Preferences} from https://github.com/yandextaxitech/binaryprefs - * - * @param name Name of preference; - * @param preferences Preferences to store value; - * @return {@link Storable} for string. - */ - @NonNull - public static Storable binaryPreferencesStringStorable(@NonNull final String name, @NonNull final Preferences preferences) { - return new Storable.Builder( - name, - String.class, - String.class, - new PreferenceStore<>(preferences), - new SameTypesConverter<>() - ).build(); - } - /** * Creates {@link NonNullStorable} that stores string into {@link SharedPreferences} with default value. *