diff --git a/build.gradle b/build.gradle
index 9599255..1d2057c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.2.3'
+ classpath 'com.android.tools.build:gradle:2.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0c1da65..6886b1f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Mar 04 17:19:53 GMT 2015
+#Tue Feb 14 14:43:09 CET 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
diff --git a/library/build.gradle b/library/build.gradle
index b4812ba..dea42fc 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -10,12 +10,12 @@ repositories {
*/
android {
- compileSdkVersion 21
- buildToolsVersion '21.1.2'
+ compileSdkVersion 25
+ buildToolsVersion '25.0.2'
defaultConfig {
minSdkVersion 10
- targetSdkVersion 21
+ targetSdkVersion 25
versionCode versionCode
versionName version
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
index e05594b..02b8e87 100755
--- a/library/src/main/AndroidManifest.xml
+++ b/library/src/main/AndroidManifest.xml
@@ -5,8 +5,8 @@
android:versionName="1.0" >
+ android:minSdkVersion="10"
+ android:targetSdkVersion="25" />
diff --git a/library/src/main/java/com/securepreferences/KeyStoreProvider.java b/library/src/main/java/com/securepreferences/KeyStoreProvider.java
new file mode 100644
index 0000000..2f0218f
--- /dev/null
+++ b/library/src/main/java/com/securepreferences/KeyStoreProvider.java
@@ -0,0 +1,224 @@
+package com.securepreferences;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.security.KeyPairGeneratorSpec;
+import android.util.Base64;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.Calendar;
+
+import javax.crypto.Cipher;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Created by Nicolas on 14.02.2017.
+ */
+
+public class KeyStoreProvider {
+
+ private final static String TAG = "KeyStoreProvider";
+
+ //Use RSA
+ private static final String RSA = "RSA";
+
+ private static final String CIPHER_INFO = "RSA/ECB/PKCS1Padding";
+
+ //Key alias
+ private static String KEY_ALIAS;
+
+ //Android Keystore
+ private static final String AndroidKeyStore = "AndroidKeyStore";
+
+ //keystore Instance
+ private KeyStore keyStore;
+
+ private boolean sLoggingEnabled;
+
+
+ private KeyPair keyPair;
+
+ private Cipher cipher;
+
+
+ public KeyStoreProvider(Context context, boolean sLoggingEnabled, String alias) throws GeneralSecurityException {
+ this.sLoggingEnabled = sLoggingEnabled;
+ if(alias == null){
+ throw new GeneralSecurityException("Alias may not be null");
+ }
+
+ //get alias(= identifier) for KeyStore
+ KEY_ALIAS = alias;
+
+ //set Cipher
+ cipher = Cipher.getInstance(CIPHER_INFO);
+
+
+
+ this.keyStore = getKeystore();
+
+
+ if (!keyStore.containsAlias(KEY_ALIAS)) {
+ generateRSAKeys(context);
+ }
+
+ // Even if we just generated the key, always read it back to ensure we can read it successfully.
+ final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
+ keyPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
+ }
+
+
+ public static boolean isAliasInKeystore(String alias) throws GeneralSecurityException
+ {
+ final KeyStore keyStore = getKeystore();
+ return keyStore.containsAlias(alias);
+ }
+
+
+ public static void deleteAlias(String alias) throws GeneralSecurityException
+ {
+ final KeyStore keyStore = getKeystore();
+ boolean aliasExists = keyStore.containsAlias(alias);
+ keyStore.deleteEntry(alias);
+ boolean aliasExitsAfterDelete = keyStore.containsAlias(alias);
+ }
+
+
+ private static KeyStore getKeystore() throws GeneralSecurityException
+ {
+ try
+ {
+ final KeyStore keyStore = KeyStore.getInstance(AndroidKeyStore);
+ keyStore.load(null);
+ return keyStore;
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ throw new GeneralSecurityException(e);
+ }
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ private boolean generateRSAKeys(Context context){
+ // Generate the RSA key pairs
+ try {
+ if (!keyStore.containsAlias(KEY_ALIAS)) {
+ // Generate a key pair for encryption
+ Calendar start = Calendar.getInstance();
+ Calendar end = Calendar.getInstance();
+ end.add(Calendar.YEAR, 30);
+ KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
+ .setAlias(KEY_ALIAS)
+ .setSubject(new X500Principal("CN=" + KEY_ALIAS))
+ .setSerialNumber(BigInteger.ONE)
+ .setStartDate(start.getTime())
+ .setEndDate(end.getTime())
+ .build();
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSA, AndroidKeyStore);
+ kpg.initialize(spec);
+ kpg.generateKeyPair();
+ return true;
+ }
+ } catch (KeyStoreException e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "KeyStoreException while generating RSA Keys: " + e.getMessage());
+ }
+ } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Invalid Algorithm: " + e.getMessage());
+ }
+ } catch (NoSuchProviderException e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Invalid Provider: " + e.getMessage());
+ }
+ }
+ return false;
+ }
+
+ public byte[] rsaEncrypt(byte[] secret) throws Exception{
+ // Encrypt the text
+ byte[] enc = null;
+ try
+ {
+ cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
+ enc = cipher.doFinal(secret);
+ }
+ //no need to catch 4 different exceptions
+ catch (Exception e)
+ {
+ if(sLoggingEnabled) {
+ Log.e(TAG,"Encrypt error: " +e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ return enc;
+ }
+
+ public byte[] rsaDecrypt(byte[] encrypted) throws Exception {
+ //Decrypt the text
+ byte[] plain = null;
+ try
+ {
+ cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
+ plain = cipher.doFinal(encrypted);
+ }
+ //no need to catch 4 different exceptions
+ catch (Exception e)
+ {
+ if(sLoggingEnabled) {
+ Log.e(TAG,"Decrypt error: " +e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+ return plain;
+ }
+
+ public String rsaDecryptWithStrings(String encrypted){
+ try {
+ byte[] encryptedBytes = Base64.decode(encrypted, Base64.NO_WRAP);
+ byte[] decryptedBytes = rsaDecrypt(encryptedBytes);
+ String decryptedString = new String(decryptedBytes, "UTF-8");
+ return decryptedString;
+ } catch (UnsupportedEncodingException e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Unsupported encoding: " + e.getMessage());
+ }
+ } catch (Exception e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Error while Decrypting: " + e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ public String rsaEncryptWithStrings(String decrypted){
+ try {
+ byte[] decryptedBytes = decrypted.getBytes("UTF-8");
+ String encryptedString = Base64.encodeToString(rsaEncrypt(decryptedBytes),Base64.NO_WRAP);
+ return encryptedString;
+ } catch (UnsupportedEncodingException e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Unsupported encoding: " + e.getMessage());
+ }
+ } catch (Exception e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Error while Decrypting: " + e.getMessage());
+ }
+ }
+ return null;
+ }
+}
diff --git a/library/src/main/java/com/securepreferences/SecurePreferences.java b/library/src/main/java/com/securepreferences/SecurePreferences.java
index db608fe..bb3913b 100755
--- a/library/src/main/java/com/securepreferences/SecurePreferences.java
+++ b/library/src/main/java/com/securepreferences/SecurePreferences.java
@@ -54,8 +54,17 @@
*/
public class SecurePreferences implements SharedPreferences {
+
private static final int ORIGINAL_ITERATION_COUNT = 10000;
+ private static final boolean ORIGINAL_USE_KEYSTORE = false;
+
+ //Use for keyStore based encryption
+ private KeyStoreProvider keyStoreProvider;
+
+ //KeyStore Alias
+ private static String KEY_ALIAS;
+
//the backing pref file
private SharedPreferences sharedPreferences;
@@ -64,6 +73,8 @@ public class SecurePreferences implements SharedPreferences {
private static boolean sLoggingEnabled = false;
+ private boolean useKeystore;
+
private static final String TAG = SecurePreferences.class.getName();
//name of the currently loaded sharedPrefFile, can be null if default
@@ -76,7 +87,7 @@ public class SecurePreferences implements SharedPreferences {
* @param context should be ApplicationContext not Activity
*/
public SecurePreferences(Context context) {
- this(context, "", null);
+ this(context, "", null, ORIGINAL_USE_KEYSTORE);
}
@@ -94,50 +105,112 @@ public SecurePreferences(Context context, int iterationCount) {
* @param sharedPrefFilename name of the shared pref file. If null use the default shared prefs
*/
public SecurePreferences(Context context, final String password, final String sharedPrefFilename) {
- this(context, null, password, sharedPrefFilename, ORIGINAL_ITERATION_COUNT);
+ this(context, null, password, sharedPrefFilename, ORIGINAL_ITERATION_COUNT, ORIGINAL_USE_KEYSTORE);
+ }
+
+ /**
+ * @param context should be ApplicationContext not Activity
+ * @param password user password/code used to generate encryption key.
+ * @param sharedPrefFilename name of the shared pref file. If null use the default shared prefs
+ * @param useKeystore should use additional KeyStore encryption
+ */
+ public SecurePreferences(Context context, final String password, final String sharedPrefFilename, boolean useKeystore) {
+ this(context, null, password, sharedPrefFilename, ORIGINAL_ITERATION_COUNT, useKeystore);
}
/**
+ * รค
+ *
* @param context should be ApplicationContext not Activity
* @param iterationCount The iteration count for the keys generation
*/
public SecurePreferences(Context context, final String password, final String sharedPrefFilename, int iterationCount) {
- this(context, null, password, sharedPrefFilename, iterationCount);
+ this(context, null, password, sharedPrefFilename, iterationCount, ORIGINAL_USE_KEYSTORE);
}
+ /**
+ * @param context should be ApplicationContext not Activity
+ * @param iterationCount The iteration count for the keys generation
+ * @param useKeystore should use additional KeyStore encryption
+ */
+ public SecurePreferences(Context context, final String password, final String sharedPrefFilename, int iterationCount, boolean useKeystore) {
+ this(context, null, password, sharedPrefFilename, iterationCount, useKeystore);
+ }
/**
* @param context should be ApplicationContext not Activity
* @param secretKey that you've generated
* @param sharedPrefFilename name of the shared pref file. If null use the default shared prefs
+ * @param useKeystore should use additional KeyStore encryption
*/
- public SecurePreferences(Context context, final AesCbcWithIntegrity.SecretKeys secretKey, final String sharedPrefFilename) {
- this(context, secretKey, null, sharedPrefFilename, 0);
+ public SecurePreferences(Context context, final AesCbcWithIntegrity.SecretKeys secretKey, final String sharedPrefFilename, boolean useKeystore) {
+ this(context, secretKey, null, sharedPrefFilename, 0, useKeystore);
}
- private SecurePreferences(Context context, final AesCbcWithIntegrity.SecretKeys secretKey, final String password, final String sharedPrefFilename, int iterationCount) {
+ private SecurePreferences(Context context, final AesCbcWithIntegrity.SecretKeys secretKey, String password, final String sharedPrefFilename, int iterationCount, boolean useKeystore) {
if (sharedPreferences == null) {
sharedPreferences = getSharedPreferenceFile(context, sharedPrefFilename);
}
+ this.useKeystore = useKeystore;
+ boolean isAliasInKeyStore=false;
+
+ if (useKeystore) {
+ KEY_ALIAS = context.getApplicationContext().getPackageName() + ".sp";
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+
+ isAliasInKeyStore = KeyStoreProvider.isAliasInKeystore(KEY_ALIAS);
+ keyStoreProvider = new KeyStoreProvider(context, sLoggingEnabled, KEY_ALIAS);
+ } else {
+ throw new GeneralSecurityException("API Level to low for using KeyStore");
+ }
+ } catch (GeneralSecurityException e) {
+ if (sLoggingEnabled) {
+ Log.e(TAG, "Error init using Keystore: " + e.getMessage());
+ }
+ throw new IllegalStateException(e);
+ }
+ }
+
+
if (secretKey != null) {
keys = secretKey;
} else if (TextUtils.isEmpty(password)) {
// Initialize or create encryption key
try {
+
+
final String key = generateAesKeyName(context, iterationCount);
String keyAsString = sharedPreferences.getString(key, null);
+
+ if (useKeystore) {
+ //see if we have a Key but no KeyStore to decrypt it
+ handlePotentialKeyStoreLoss(keyAsString,isAliasInKeyStore);
+ }
+
if (keyAsString == null) {
keys = AesCbcWithIntegrity.generateKey();
//saving new key
- boolean committed = sharedPreferences.edit().putString(key, keys.toString()).commit();
+ boolean committed;
+ if (useKeystore) {
+ //save Key encrypted with KeyStore RSA
+ committed = sharedPreferences.edit().putString(key, keyStoreProvider.rsaEncryptWithStrings(keys.toString())).commit();
+ } else {
+ committed = sharedPreferences.edit().putString(key, keys.toString()).commit();
+ }
+
if (!committed) {
Log.w(TAG, "Key not committed to prefs");
}
} else {
- keys = AesCbcWithIntegrity.keys(keyAsString);
+ if (useKeystore) {
+ keys = AesCbcWithIntegrity.keys(keyStoreProvider.rsaDecryptWithStrings(keyAsString));
+ } else {
+ keys = AesCbcWithIntegrity.keys(keyAsString);
+ }
}
if (keys == null) {
@@ -154,7 +227,21 @@ private SecurePreferences(Context context, final AesCbcWithIntegrity.SecretKeys
//use the password to generate the key
try {
final byte[] salt = getDeviceSerialNumber(context).getBytes();
- keys = AesCbcWithIntegrity.generateKeyFromPassword(password, salt, iterationCount);
+ //get additional Material to be added to the password and save it encrypted via the keystore, this makes a bruteforce harder
+ String additionalMaterial = "";
+ if (useKeystore) {
+ handlePotentialKeyStoreLoss(password,isAliasInKeyStore);
+ String accessKey = generateAesKeyName(context, iterationCount);
+ additionalMaterial = keyStoreProvider.rsaDecryptWithStrings(sharedPreferences.getString(accessKey, ""));
+
+ if (TextUtils.isEmpty(additionalMaterial)) {
+ additionalMaterial = AesCbcWithIntegrity.generateKey().toString();
+ String encryptedMaterial = keyStoreProvider.rsaEncryptWithStrings(additionalMaterial);
+ sharedPreferences.edit().putString(accessKey, encryptedMaterial).commit();
+ }
+ }
+
+ keys = AesCbcWithIntegrity.generateKeyFromPassword(password + additionalMaterial, salt, iterationCount);
if (keys == null) {
throw new GeneralSecurityException("Problem generating Key From Password");
@@ -166,6 +253,21 @@ private SecurePreferences(Context context, final AesCbcWithIntegrity.SecretKeys
throw new IllegalStateException(e);
}
}
+}
+
+
+ /**
+ * Handle potential loss of KeyStore due to Pin/Password change on device, will delete all values
+ *
+ * @param testString test password or encryption String for existence
+ * @param isAliasInKeyStore is alias in keystore
+ */
+ private void handlePotentialKeyStoreLoss(String testString, boolean isAliasInKeyStore) throws GeneralSecurityException {
+ if (!isAliasInKeyStore && !TextUtils.isEmpty(testString)) {
+ //handle lockscreen password change
+ destroyKeys();
+ sharedPreferences.edit().clear().commit();
+ }
}
@@ -211,6 +313,8 @@ private String generateAesKeyName(Context context, int iterationCount) throws Ge
}
+
+
/**
* Gets the hardware serial number of this device.
*
diff --git a/sample/src/com/securepreferences/sample/App.java b/sample/src/com/securepreferences/sample/App.java
index bbd6584..c74a001 100644
--- a/sample/src/com/securepreferences/sample/App.java
+++ b/sample/src/com/securepreferences/sample/App.java
@@ -7,7 +7,6 @@
import android.util.Log;
import com.securepreferences.SecurePreferences;
-import com.securepreferences.sample.utils.TickTock;
import com.tozny.crypto.android.AesCbcWithIntegrity;
import java.security.GeneralSecurityException;
@@ -39,7 +38,7 @@ public static App get() {
@DebugLog
public SharedPreferences getSharedPreferences() {
if(mSecurePrefs==null){
- mSecurePrefs = new SecurePreferences(this, "", "my_prefs.xml");
+ mSecurePrefs = new SecurePreferences(this, "", "my_prefs.xml",false);
SecurePreferences.setLoggingEnabled(true);
}
return mSecurePrefs;
@@ -54,7 +53,7 @@ public SharedPreferences getSharedPreferences() {
public SharedPreferences getSharedPreferences1000() {
try {
AesCbcWithIntegrity.SecretKeys myKey = AesCbcWithIntegrity.generateKeyFromPassword(Build.SERIAL,AesCbcWithIntegrity.generateSalt(),1000);
- return new SecurePreferences(this, myKey, "my_prefs_1000.xml");
+ return new SecurePreferences(this, myKey, "my_prefs_1000.xml",false);
} catch (GeneralSecurityException e) {
Log.e(TAG, "Failed to create custom key for SecurePreferences", e);
}
@@ -70,7 +69,7 @@ public SharedPreferences getDefaultSharedPreferences() {
@DebugLog
public SecurePreferences getUserPinBasedSharedPreferences(String password){
if(mUserPrefs==null) {
- mUserPrefs = new SecurePreferences(this, password, "user_prefs.xml");
+ mUserPrefs = new SecurePreferences(this, password, "user_prefs.xml",false);
}
return mUserPrefs;
}