Merge pull request #130 from TouchInstinct/enc-shared-prefs

Enc shared prefs
This commit is contained in:
Aksenov Vladimir 2020-06-10 18:26:05 +03:00 committed by GitHub
commit 27d953a254
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 253 additions and 23 deletions

1
encrypted-shared-prefs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.roboswag.sharedprefs" />

View File

@ -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
}

View File

@ -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<String, String> {
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<String>?): MutableSet<String>? {
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 <T> 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<String>?): 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 <T> put(key: String?, value: T) = currentPreferences.edit().putString(key, value.toString().encrypt())
private fun String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this
}
}

View File

@ -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) } ?: ""
}
}

View File

@ -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"
}

View File

@ -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<String, String, String> binaryPreferencesStringStorable(@NonNull final String name, @NonNull final Preferences preferences) {
return new Storable.Builder<String, String, String>(
name,
String.class,
String.class,
new PreferenceStore<>(preferences),
new SameTypesConverter<>()
).build();
}
/**
* Creates {@link NonNullStorable} that stores string into {@link SharedPreferences} with default value.
*