Merge pull request #130 from TouchInstinct/enc-shared-prefs
Enc shared prefs
This commit is contained in:
commit
27d953a254
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.roboswag.sharedprefs" />
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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) } ?: ""
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue