Skip to content

Instantly share code, notes, and snippets.

@AnthonyWharton
Created June 25, 2018 17:03
Show Gist options
  • Save AnthonyWharton/406b6efe1241473a2afcb02b3712078d to your computer and use it in GitHub Desktop.
Save AnthonyWharton/406b6efe1241473a2afcb02b3712078d to your computer and use it in GitHub Desktop.
A more "secure", obfuscating wrapper for Android's SharedPreferences
// Copyright 2018 Anthony Wharton
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO, Add your Package here
// package ...
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.support.annotation.RequiresApi
import android.util.Base64
import android.util.Log
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.MessageDigest
import java.security.PrivateKey
import java.util.Calendar
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.security.auth.x500.X500Principal
/*
* Class that provides a wrapper to Android's SharedPreferences and
* SharedPreferences.Editor, that encrypts the information being stored with
* under an AES/CBC/PKCS7Padding scheme. Encryption keys are kept in the
* Android KeyStore.
*
* Should a device be on SDK < 23 (M), then AES keys use device default
* implementations and parameters, and are wrapped with an RSA 2048 bit key
* which is stored in the Android KeyStore. The wrapped key is stored in
* SharedPreferences.
*
* All "keys" for values in SharedPreferences are referred to as tags to avoid
* any confusion between those and encryption keys. Tags are hashed with
* SHA-256, and are unique as per the type of the stored item.
*
* All functions from SharedPreferences and SharedPreferences.Editor are
* available other than the following:
* - getAll()
* - getStringSet(...)
* - Editor.putStringSet(...)
* - registerOnSharedPreferenceChangeListener(...)
* - unregisterOnSharedPreferenceChangeListener(...)
* TODO: Add suport for String Sets
*
* Tested with SDK 22+, should work with 18+ unless I was a bit naughty and
* used a function I shouldn't have in the wrong place. (TODO: Check this)
*
* IMPORTANT NOTE: This is not a means for securing the data in
* SharedPreferences, but more as a slightly more secure obfuscation technique.
*/
class Preferences(context: Context) {
companion object {
private const val TAG = "Preferences"
private const val PREFERENCES_ID = "uh-net.keystore.secure-prefs"
private const val KEYSTORE_ALIAS = "uh-net.keystore.shared-prefs"
private const val RSA_BITS = 2048
private const val IV_SEP = "~"
// Used for the tags and values stored in SharedPreferences
private const val PREFIX_BOOL = "B"
private const val PREFIX_FLOAT = "F"
private const val PREFIX_INT = "I"
private const val PREFIX_LONG = "L"
private const val PREFIX_STRING = "S"
private const val NO_VALUE = "_N_0_N_E_"
private const val LEGACY_AES_TAG = "_LEG4CY_M0DE_WRAPPED_AES_KEY_"
// TODO Generate a random tag at runtime
private val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
private val wrapCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
private val h = MessageDigest.getInstance("SHA-256")
// Encrypts a string with the given secret key.
private fun encryptString(data: String, key: SecretKey): String {
cipher.init(Cipher.ENCRYPT_MODE, key)
val output = cipher.doFinal(data.toByteArray())
val iv = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
return iv + IV_SEP + Base64.encodeToString(output, Base64.DEFAULT)
}
// Decrypts a string with the given secret key.
private fun decryptString(data: String, key: SecretKey): String {
val split = data.split(IV_SEP.toRegex())
if (split.size != 2)
throw IllegalArgumentException("Input data is of incorrect form")
val iv = Base64.decode(split[0], Base64.DEFAULT)
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
val output = cipher.doFinal(Base64.decode(split[1], Base64.DEFAULT))
return String(output)
}
// Hashes the given tag with SHA-256
private fun hashTag(tag: String): String = Base64.encodeToString(
h.apply {reset()}.digest(tag.toByteArray()),
Base64.DEFAULT
)
}
private val prefs = context.getSharedPreferences(PREFERENCES_ID, Context.MODE_PRIVATE)
// Checks if we have a key in the KeyStore, if so nothing happens, if not
// then new keys are generated by the initialise_helper functions
init {
if (!getKeyStore().containsAlias(KEYSTORE_ALIAS)) {
Log.w(TAG, "No key found for secure preferences, generating key(s)...")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
initialise_helper()
} else {
initialise_helper_legacy(context)
}
}
}
// Generates a symmetric encryption key, and stores it in the Android
// KeyStore.
@RequiresApi(Build.VERSION_CODES.M)
private fun initialise_helper() {
val kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(
KEYSTORE_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_CBC)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
setKeySize(128)
}
kg.init(builder.build())
kg.generateKey()
}
// Generates an asymmetric key pair that is stored in the Android KeyStore,
// and then uses that to wrap a generated symmetric key and store securely
// in Android's SharedPreferences. Slower, and only used on < SDK 23
private fun initialise_helper_legacy(context: Context) {
// Generate and store RSA keypair for wrapping AES Key
val kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore")
val calStart = Calendar.getInstance()
val calEnd = Calendar.getInstance()
calEnd.add(Calendar.YEAR, 20)
kpg.initialize(KeyPairGeneratorSpec.Builder(context)
.setAlias(KEYSTORE_ALIAS)
.setSubject(X500Principal("CN=Secure Preferences Certificate")
.setSerialNumber(BigInteger.ONE)
.setStartDate(calStart.time)
.setEndDate(calEnd.time)
.setKeySize(RSA_BITS)
.build()
)
val keyPair = kpg.generateKeyPair()
// Generate and store wrapped AES key
val aesKey = KeyGenerator.getInstance("AES").generateKey()
wrapCipher.init(Cipher.WRAP_MODE, keyPair.public)
val encodedKey = Base64.encodeToString(wrapCipher.wrap(aesKey), Base64.DEFAULT)
prefs.edit().putString(hashTag(LEGACY_AES_TAG), encodedKey).apply()
}
// Gets a loaded AndroidKeyStore instance
private fun getKeyStore() = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
// Gets the symmetric key for encryption/decryption
// If in legacy mode, will unwrap the stored key
private fun getKey(): SecretKey {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return getKeyStore().getKey(KEYSTORE_ALIAS, null) as SecretKey?
?: throw RuntimeException("Key not found in KeyStore")
} else {
val encryptedKey = prefs.getString(hashTag(LEGACY_AES_TAG), "error")
if (encryptedKey != "error") {
val encryptedKeyData = Base64.decode(encryptedKey, Base64.DEFAULT)
val key = getKeyStore().getKey(KEYSTORE_ALIAS, null) as PrivateKey?
?: throw RuntimeException("Wrap Key not found in KeyStore")
wrapCipher.init(Cipher.UNWRAP_MODE, key)
return wrapCipher.unwrap(encryptedKeyData,
"AES",
Cipher.SECRET_KEY) as SecretKey?
?: throw RuntimeException("Unable to unwrap key")
} else {
throw RuntimeException("Wrapped AES key not found")
}
}
}
fun contains(tag: String): Boolean =
prefs.contains(hashTag(PREFIX_BOOL + tag)) or
prefs.contains(hashTag(PREFIX_FLOAT + tag)) or
prefs.contains(hashTag(PREFIX_INT + tag)) or
prefs.contains(hashTag(PREFIX_LONG + tag)) or
prefs.contains(hashTag(PREFIX_STRING + tag))
fun edit(): Preferences.Editor = Editor(prefs, getKey())
fun getBoolean(tag: String, default: Boolean): Boolean {
val encTag = hashTag(PREFIX_BOOL + tag)
val encData = prefs.getString(encTag, NO_VALUE)
if (encData == NO_VALUE) return default
return decryptString(encData, getKey()).drop(36).toBoolean()
}
fun getFloat(tag: String, default: Float): Float {
val encTag = hashTag(PREFIX_FLOAT + tag)
val encData = prefs.getString(encTag, NO_VALUE)
if (encData == NO_VALUE) return default
return decryptString(encData, getKey()).drop(36).toFloat()
}
fun getInt(tag: String, default: Int): Int {
val encTag = hashTag(PREFIX_INT + tag)
val encData = prefs.getString(encTag, NO_VALUE)
if (encData == NO_VALUE) return default
return decryptString(encData, getKey()).drop(36).toInt()
}
fun getLong(tag: String, default: Long): Long {
val encTag = hashTag(PREFIX_LONG + tag)
val encData = prefs.getString(encTag, NO_VALUE)
if (encData == NO_VALUE) return default
return decryptString(encData, getKey()).drop(36).toLong()
}
fun getString(tag: String, default: String): String {
val encTag = hashTag(PREFIX_STRING + tag)
val encData = prefs.getString(encTag, NO_VALUE)
if (encData == NO_VALUE) return default
return decryptString(encData, getKey()).drop(36)
}
class Editor(prefs: SharedPreferences, private val key: SecretKey) {
val editor = prefs.edit()
private fun randomPrefix(): String = UUID.randomUUID().toString()
fun putBoolean(tag: String, data: Boolean) {
val encTag = hashTag(PREFIX_BOOL + tag)
val encData = encryptString(randomPrefix() + data.toString(), key)
editor.putString(encTag, encData)
}
fun putFloat(tag: String, data: Float) {
val encTag = hashTag(PREFIX_FLOAT + tag)
val encData = encryptString(randomPrefix() + data.toString(), key)
editor.putString(encTag, encData)
}
fun putInt(tag: String, data: Int) {
val encTag = hashTag(PREFIX_INT + tag)
val encData = encryptString(randomPrefix() + data.toString(), key)
editor.putString(encTag, encData)
}
fun putLong(tag: String, data: Long) {
val encTag = hashTag(PREFIX_LONG + tag)
val encData = encryptString(randomPrefix() + data.toString(), key)
editor.putString(encTag, encData)
}
fun putString(tag: String, data: String) {
val encTag = hashTag(PREFIX_STRING + tag)
val encData = encryptString(randomPrefix() + data, key)
editor.putString(encTag, encData)
}
fun apply() = editor.apply()
fun clear() = editor.clear()
fun commit() = editor.commit()
fun remove(tag:String) = editor.remove(tag)
}
// TODO, Make proper unit tests
private fun selfTest(): Boolean {
val d = "abcdefghijklmnop".repeat(128)
val editor = edit()
editor.putBoolean("test", true)
editor.putFloat("test", Float.MAX_VALUE)
editor.putInt("test", Int.MAX_VALUE)
editor.putLong("test", Long.MAX_VALUE)
editor.putString("test", d)
editor.apply()
return getBoolean("test", false).equals(true) or
getFloat("test", 0.0f).equals(Float.MAX_VALUE) or
getInt("test", 0).equals(Int.MAX_VALUE) or
getLong("test", 0L).equals(Long.MAX_VALUE) or
getString("test", d).equals(d)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment