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