Utils: Add a basic implementation of an EncryptionManager which allows to store the password encrypted.
This use the latest AndroidKeyStore from Android M and allow to store the key securely.
--- a/app/src/main/java/com/beem/project/beem/BeemApplication.java Fri Nov 13 18:15:37 2015 +0100
+++ b/app/src/main/java/com/beem/project/beem/BeemApplication.java Fri Nov 13 18:17:25 2015 +0100
@@ -66,6 +66,10 @@
public static final String ACCOUNT_USERNAME_KEY = "account_username";
/** Preference key for account password. */
public static final String ACCOUNT_PASSWORD_KEY = "account_password";
+ /** Preference key to know if the account password is encrypted. */
+ public static final String ACCOUNT_PASSWORD_IS_ENCRYPTED_KEY = "account_password_is_encrypted";
+ /** Preference key to store the the account password encryption IV. */
+ public static final String ACCOUNT_PASSWORD_ENCRYPTION_IV = "account_password_encryption_iv";
/** Preference key set to true if using an Android account . */
public static final String USE_SYSTEM_ACCOUNT_KEY = "use_system_account";
--- a/app/src/main/java/com/beem/project/beem/service/auth/PreferenceAuthenticator.java Fri Nov 13 18:15:37 2015 +0100
+++ b/app/src/main/java/com/beem/project/beem/service/auth/PreferenceAuthenticator.java Fri Nov 13 18:17:25 2015 +0100
@@ -33,6 +33,7 @@
import android.preference.PreferenceManager;
import com.beem.project.beem.BeemApplication;
+import com.beem.project.beem.utils.EncryptionManager;
import org.apache.harmony.javax.security.auth.callback.Callback;
import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
@@ -40,6 +41,7 @@
import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.harmony.javax.security.sasl.RealmCallback;
+import org.jivesoftware.smack.util.Base64;
import org.jivesoftware.smack.util.StringUtils;
/**
@@ -48,6 +50,7 @@
public class PreferenceAuthenticator implements CallbackHandler {
private final SharedPreferences settings;
+ private final EncryptionManager encryptionManager;
/**
* Create a PreferenceAuthenticator.
@@ -56,7 +59,8 @@
*/
public PreferenceAuthenticator(final Context context) {
settings = PreferenceManager.getDefaultSharedPreferences(context);
- }
+ encryptionManager = new EncryptionManager();
+ }
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
@@ -79,7 +83,7 @@
String prompt = pcb.getPrompt();
if (prompt != null && prompt.startsWith("PKCS11 Password:"))
continue;
- String password = settings.getString(BeemApplication.ACCOUNT_PASSWORD_KEY, "");
+ String password = getPassword();
pcb.setPassword(password.toCharArray());
} else if (callbacks[i] instanceof RealmCallback) {
RealmCallback rcb = (RealmCallback) callbacks[i];
@@ -90,4 +94,14 @@
}
}
+ private String getPassword() {
+ String password = settings.getString(BeemApplication.ACCOUNT_PASSWORD_KEY, "");
+ if (settings.getBoolean(BeemApplication.ACCOUNT_PASSWORD_IS_ENCRYPTED_KEY, false)) {
+ byte[] encryptionIv = Base64.decode(settings.getString(BeemApplication.ACCOUNT_PASSWORD_ENCRYPTION_IV, ""));
+
+ password = encryptionManager.decryptString(password, "Beem-password-key", encryptionIv);
+ }
+ return password;
+ }
+
}
--- a/app/src/main/java/com/beem/project/beem/ui/wizard/AccountConfigureFragment.java Fri Nov 13 18:15:37 2015 +0100
+++ b/app/src/main/java/com/beem/project/beem/ui/wizard/AccountConfigureFragment.java Fri Nov 13 18:17:25 2015 +0100
@@ -74,6 +74,7 @@
import com.beem.project.beem.R;
import com.beem.project.beem.ui.Login;
import com.beem.project.beem.ui.Settings;
+import com.beem.project.beem.utils.EncryptionManager;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionConfiguration;
@@ -81,6 +82,7 @@
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.proxy.ProxyInfo;
import org.jivesoftware.smack.proxy.ProxyInfo.ProxyType;
+import org.jivesoftware.smack.util.Base64;
import org.jivesoftware.smack.util.StringUtils;
/**
@@ -110,6 +112,7 @@
private String mSelectedAccountType;
private SharedPreferences settings;
private boolean useSystemAccount;
+ private EncryptionManager encryptionManager;
private com.beem.project.beem.ui.wizard.AccountConfigureFragment.ConnectionTestTask task;
@@ -129,7 +132,8 @@
setRetainInstance(true);
settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
- }
+ encryptionManager = new EncryptionManager();
+ }
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -288,9 +292,24 @@
*
*/
private void saveCredential(String jid, String pass) {
- SharedPreferences.Editor edit = settings.edit();
+ SharedPreferences.Editor edit = settings.edit();
+ String encryptedPass = pass;
+ boolean isEncryptedPass = false;
+ if (encryptionManager.isEncryptionAvailable()) {
+ if (!encryptionManager.hasEncryptionKey("Beem-password-key")) {
+ encryptionManager.generateEncryptionKey("Beem-password-key");
+ }
+ encryptedPass = encryptionManager.encryptString(encryptedPass, "Beem-password-key");
+ if (encryptedPass != null) {
+ String encryptionIV = Base64.encodeBytes(encryptionManager.getLatestEncryptionIv());
+ edit.putString(BeemApplication.ACCOUNT_PASSWORD_ENCRYPTION_IV, encryptionIV);
+ isEncryptedPass = true;
+ }
+ }
edit.putString(BeemApplication.ACCOUNT_USERNAME_KEY, jid);
- edit.putString(BeemApplication.ACCOUNT_PASSWORD_KEY, pass);
+ edit.putString(BeemApplication.ACCOUNT_PASSWORD_KEY, encryptedPass);
+
+ edit.putBoolean(BeemApplication.ACCOUNT_PASSWORD_IS_ENCRYPTED_KEY, isEncryptedPass);
edit.putBoolean(BeemApplication.USE_SYSTEM_ACCOUNT_KEY, false);
edit.commit();
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/java/com/beem/project/beem/utils/EncryptionManager.java Fri Nov 13 18:17:25 2015 +0100
@@ -0,0 +1,128 @@
+package com.beem.project.beem.utils;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+
+import com.google.android.apps.iosched.util.LogUtils;
+
+import org.jivesoftware.smack.util.Base64;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+
+import static com.google.android.apps.iosched.util.LogUtils.LOGD;
+import static com.google.android.apps.iosched.util.LogUtils.LOGW;
+
+public class EncryptionManager {
+
+ private static final String TAG = LogUtils.makeLogTag(EncryptionManager.class);
+ private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
+ private static final String CIPHER_TRANSFORMATION = String.format("%s/%s/%s",
+ KeyProperties.KEY_ALGORITHM_AES, KeyProperties.BLOCK_MODE_CBC,
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
+ private boolean isEncryptionAvailable;
+ private KeyStore keystore;
+ private KeyGenerator keyGenerator;
+ private Cipher aesCipher;
+ private byte[] latestEncryptionIv;
+
+ public EncryptionManager() {
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ keystore = KeyStore.getInstance(ANDROID_KEY_STORE);
+ keystore.load(null);
+ aesCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
+ isEncryptionAvailable = true;
+ } else {
+ isEncryptionAvailable = false;
+ }
+ } catch (NoSuchPaddingException | KeyStoreException | IOException | NoSuchProviderException | NoSuchAlgorithmException | CertificateException e) {
+ LOGW(TAG, "Unable to load AndroidKeyStore", e);
+ isEncryptionAvailable = false;
+ }
+ }
+
+ public boolean isEncryptionAvailable() {
+ return isEncryptionAvailable;
+ }
+
+ public boolean hasEncryptionKey(String alias) {
+ if (!isEncryptionAvailable) {
+ return false;
+ }
+ try {
+ return keystore.getKey(alias, null) != null;
+ } catch (GeneralSecurityException e) {
+ LOGD(TAG, "Unable to get key", e);
+ return false;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public boolean generateEncryptionKey(String alias) {
+ if (!isEncryptionAvailable)
+ return false;
+ KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ .setUserAuthenticationRequired(false)
+ .build();
+ try {
+ keyGenerator.init(spec);
+ return keyGenerator.generateKey() != null;
+ } catch (InvalidAlgorithmParameterException e) {
+ LOGW(TAG, "Unable to generate key", e);
+ return false;
+ }
+ }
+
+ public String encryptString(String cleartext, String keyAlias) {
+ if (!isEncryptionAvailable())
+ return null;
+ try {
+ Key key = keystore.getKey(keyAlias, null);
+ aesCipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] encrypted = aesCipher.doFinal(cleartext.getBytes());
+ latestEncryptionIv = aesCipher.getIV();
+ return Base64.encodeBytes(encrypted);
+ } catch (GeneralSecurityException e) {
+ LOGW(TAG, "Unable to encrypt text", e);
+ return null;
+ }
+ }
+
+ public byte[] getLatestEncryptionIv() {
+ return latestEncryptionIv;
+ }
+
+ public String decryptString(String password, String keyAlias, byte[] encryptionIv) {
+ if (!isEncryptionAvailable())
+ return null;
+ try {
+ Key key = keystore.getKey(keyAlias, null);
+ byte[] passwordByte = Base64.decode(password);
+ aesCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encryptionIv));
+ byte[] clear = aesCipher.doFinal(passwordByte);
+ return new String(clear);
+ } catch (GeneralSecurityException e) {
+ LOGW(TAG, "Unable to decrypt text", e);
+ return null;
+ }
+ }
+}