From 2e09eb42ad6dc99be31ac2552a94c71a3aa2c60e Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 6 Oct 2017 14:10:06 +0200 Subject: [PATCH] Store the backup password encrypted Closes #49 --- .../andotp/Activities/BackupActivity.java | 6 +- ....java => PasswordEncryptedPreference.java} | 43 ++++++++++-- .../andotp/Utilities/DatabaseHelper.java | 2 +- .../andotp/Utilities/EncryptionHelper.java | 51 +++++++------- .../andotp/Utilities/KeyStoreHelper.java | 66 +++++++------------ .../flocke/andotp/Utilities/Settings.java | 36 ++++++++++ app/src/main/res/values/settings.xml | 1 + app/src/main/res/xml/preferences.xml | 4 +- 8 files changed, 134 insertions(+), 75 deletions(-) rename app/src/main/java/org/shadowice/flocke/andotp/Preferences/{PasswordPreference.java => PasswordEncryptedPreference.java} (79%) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index a17a7f9b..1cd1284d 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -133,7 +133,7 @@ public class BackupActivity extends BaseActivity { LinearLayout backupCrypt = v.findViewById(R.id.button_backup_crypt); LinearLayout restoreCrypt = v.findViewById(R.id.button_restore_crypt); - if (settings.getBackupPassword().isEmpty()) { + if (settings.getBackupPasswordEnc().isEmpty()) { cryptSetup.setVisibility(View.VISIBLE); backupCrypt.setVisibility(View.GONE); restoreCrypt.setVisibility(View.GONE); @@ -392,7 +392,7 @@ public class BackupActivity extends BaseActivity { /* Encrypted backup functions */ private void doRestoreCrypt(Uri uri) { - String password = settings.getBackupPassword(); + String password = settings.getBackupPasswordEnc(); if (! password.isEmpty()) { if (StorageHelper.isExternalStorageReadable()) { @@ -428,7 +428,7 @@ public class BackupActivity extends BaseActivity { } private void doBackupCrypt(Uri uri) { - String password = settings.getBackupPassword(); + String password = settings.getBackupPasswordEnc(); if (! password.isEmpty()) { if (StorageHelper.isExternalStorageWritable()) { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java similarity index 79% rename from app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordPreference.java rename to app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java index e73fad22..e4a8fe0d 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordPreference.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java @@ -33,19 +33,28 @@ import android.text.InputType; import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; import android.util.AttributeSet; +import android.util.Base64; import android.view.View; import android.widget.Button; import android.widget.EditText; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; -public class PasswordPreference extends DialogPreference +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; + +public class PasswordEncryptedPreference extends DialogPreference implements View.OnClickListener, TextWatcher { public enum Mode { PASSWORD, PIN } + public static final String KEY_ALIAS = "password"; + private KeyPair key; + private static final String DEFAULT_VALUE = ""; private Mode mode = Mode.PASSWORD; @@ -57,9 +66,15 @@ public class PasswordPreference extends DialogPreference private String value = DEFAULT_VALUE; - public PasswordPreference(Context context, AttributeSet attrs) { + public PasswordEncryptedPreference(Context context, AttributeSet attrs) { super(context, attrs); + try { + key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS); + } catch (Exception e) { + e.printStackTrace(); + } + setDialogLayoutResource(R.layout.component_password); } @@ -120,13 +135,31 @@ public class PasswordPreference extends DialogPreference return a.getString(index); } + private void encryptAndPersist(String value) { + try { + byte[] encBytes = EncryptionHelper.encrypt(key.getPublic(), value.getBytes(StandardCharsets.UTF_8)); + persistString(Base64.encodeToString(encBytes, Base64.URL_SAFE)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void restoreAndDecrypt(String encValue) { + try { + byte[] encBytes = Base64.decode(encValue, Base64.URL_SAFE); + value = new String(EncryptionHelper.decrypt(key.getPrivate(), encBytes), StandardCharsets.UTF_8); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { if (restorePersistedValue) { - value = getPersistedString(DEFAULT_VALUE); + restoreAndDecrypt(getPersistedString(DEFAULT_VALUE)); } else { value = (String) defaultValue; - persistString(value); + encryptAndPersist(value); } } @@ -138,7 +171,7 @@ public class PasswordPreference extends DialogPreference break; case (R.id.btnSave): value = passwordInput.getText().toString(); - persistString(value); + encryptAndPersist(value); getDialog().dismiss(); break; diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java index 9142881c..68c3c3e1 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java @@ -47,7 +47,7 @@ public class DatabaseHelper { byte[] data = jsonString.getBytes(); SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); - data = EncryptionHelper.encrypt(key,data); + data = EncryptionHelper.encrypt(key, data); FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + SETTINGS_FILE), data); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java index 17b49751..4eb860bf 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java @@ -29,6 +29,8 @@ import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.SecureRandom; import java.util.Arrays; @@ -41,7 +43,9 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class EncryptionHelper { - private final static String ALGORITHM = "AES/GCM/NoPadding"; + private final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding"; + private final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; + private final static int IV_LENGTH = 12; public static SecretKey generateSymmetricKeyFromPassword(String password) @@ -51,30 +55,14 @@ public class EncryptionHelper { return new SecretKeySpec(sha.digest(password.getBytes(StandardCharsets.UTF_8)), "AES"); } - public static byte[] encrypt(SecretKey secretKey, IvParameterSpec iv, byte[] plainText) - throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); - - return cipher.doFinal(plainText); - } - - public static byte[] decrypt(SecretKey secretKey, IvParameterSpec iv, byte[] cipherText) - throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); - - return cipher.doFinal(cipherText); - } - - public static byte[] encrypt(SecretKey secretKey, byte[] plaintext) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException { final byte[] iv = new byte[IV_LENGTH]; new SecureRandom().nextBytes(iv); - byte[] cipherText = encrypt(secretKey, new IvParameterSpec(iv), plaintext); - + Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); + byte[] cipherText = cipher.doFinal(plaintext); byte[] combined = new byte[iv.length + cipherText.length]; System.arraycopy(iv, 0, combined, 0, iv.length); @@ -83,11 +71,30 @@ public class EncryptionHelper { return combined; } + public static byte[] encrypt(PublicKey publicKey, byte[] plaintext) + throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException { + Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + + return cipher.doFinal(plaintext); + } + public static byte[] decrypt(SecretKey secretKey, byte[] cipherText) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { byte[] iv = Arrays.copyOfRange(cipherText, 0, IV_LENGTH); - byte[] cipher = Arrays.copyOfRange(cipherText, IV_LENGTH,cipherText.length); + byte[] encrypted = Arrays.copyOfRange(cipherText, IV_LENGTH, cipherText.length); - return decrypt(secretKey,new IvParameterSpec(iv), cipher ); + Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); + + return cipher.doFinal(encrypted); + } + + public static byte[] decrypt(PrivateKey privateKey, byte[] cipherText) + throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { + Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + + return cipher.doFinal(cipherText); } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index bb27e11b..6ea4854c 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -32,7 +32,6 @@ import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.security.GeneralSecurityException; -import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; @@ -41,7 +40,6 @@ import java.security.spec.AlgorithmParameterSpec; import java.util.Calendar; import java.util.GregorianCalendar; -import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.x500.X500Principal; @@ -49,39 +47,38 @@ import javax.security.auth.x500.X500Principal; public class KeyStoreHelper { private final static int KEY_LENGTH = 16; - private static AlgorithmParameterSpec generateAlgorithmSpec(Context context, String alias) { - final Calendar start = new GregorianCalendar(); - final Calendar end = new GregorianCalendar(); - end.add(Calendar.YEAR, 100); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setCertificateSubject(new X500Principal("CN=" + alias)) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - .setCertificateSerialNumber(BigInteger.ONE) - .setCertificateNotBefore(start.getTime()) - .setCertificateNotAfter(end.getTime()) - .build(); - } else { - return new KeyPairGeneratorSpec.Builder(context) - .setAlias(alias) - .setSubject(new X500Principal("CN=" + alias)) - .setSerialNumber(BigInteger.ONE) - .setStartDate(start.getTime()) - .setEndDate(end.getTime()) - .build(); - } - } - public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias) throws GeneralSecurityException, IOException { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); if (! keyStore.containsAlias(alias)) { + final Calendar start = new GregorianCalendar(); + final Calendar end = new GregorianCalendar(); + end.add(Calendar.YEAR, 100); + + AlgorithmParameterSpec spec; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + spec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setCertificateSubject(new X500Principal("CN=" + alias)) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + .setCertificateSerialNumber(BigInteger.ONE) + .setCertificateNotBefore(start.getTime()) + .setCertificateNotAfter(end.getTime()) + .build(); + } else { + spec = new KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(new X500Principal("CN=" + alias)) + .setSerialNumber(BigInteger.ONE) + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + } + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); - gen.initialize(generateAlgorithmSpec(context, alias)); + gen.initialize(spec); gen.generateKeyPair(); } @@ -89,21 +86,6 @@ public class KeyStoreHelper { return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); } - public static Key loadOrGenerateSymmetricKey(Context context, String alias) - throws GeneralSecurityException, IOException { - final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); - keyStore.load(null); - - if (! keyStore.containsAlias(alias)) { - KeyGenerator gen = KeyGenerator.getInstance("AES", "AndroidKeyStore"); - - gen.init(generateAlgorithmSpec(context, alias)); - gen.generateKey(); - } - - return keyStore.getKey(alias, null); - } - /** * Load our symmetric secret key. * The symmetric secret key is stored securely on disk by wrapping diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index d65e0faa..cf71d6f5 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -25,14 +25,19 @@ package org.shadowice.flocke.andotp.Utilities; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.util.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import org.shadowice.flocke.andotp.R; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; import java.util.Collections; import java.util.Set; +import static org.shadowice.flocke.andotp.Preferences.PasswordEncryptedPreference.KEY_ALIAS; + public class Settings { private Context context; private SharedPreferences settings; @@ -70,6 +75,21 @@ public class Settings { remove(R.string.settings_key_auth_pin); } + + if (settings.contains(getResString(R.string.settings_key_backup_password))) { + String plainPassword = getBackupPassword(); + + try { + KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS); + byte[] encPassword = EncryptionHelper.encrypt(key.getPublic(), plainPassword.getBytes(StandardCharsets.UTF_8)); + + setString(R.string.settings_key_backup_password_enc, Base64.encodeToString(encPassword, Base64.URL_SAFE)); + + remove(R.string.settings_key_backup_password); + } catch (Exception e) { + e.printStackTrace(); + } + } } private String getResString(int resId) { @@ -195,6 +215,22 @@ public class Settings { return getString(R.string.settings_key_backup_password, ""); } + public String getBackupPasswordEnc() { + String base64Password = getString(R.string.settings_key_backup_password_enc, ""); + byte[] encPassword = Base64.decode(base64Password, Base64.URL_SAFE); + + String password = ""; + + try { + KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS); + password = new String(EncryptionHelper.decrypt(key.getPrivate(), encPassword), StandardCharsets.UTF_8); + } catch (Exception e) { + e.printStackTrace(); + } + + return password; + } + public String getOpenPGPProvider() { return getString(R.string.settings_key_openpgp_provider, ""); } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 37b89af2..58826158 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -15,6 +15,7 @@ pref_label_size pref_backup_password + pref_backup_password_enc pref_openpgp_provider pref_openpgp_keyid pref_openpgp_sign diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 2d5564da..89dbc5af 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -57,8 +57,8 @@ -