From d96b0373d9db6af6363aec59bf3a32928f0c504d Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Mon, 16 Sep 2019 12:25:43 +0200 Subject: [PATCH] Improve security of the password-protected backups This adds a new encryption for the password-protected backups that uses a proper key derivation function (PBKDF2). Import of the old backups is still supported, new backups are forced to use the new encryption. --- .../andotp/Activities/BackupActivity.java | 71 +++++++++++++++---- .../flocke/andotp/Utilities/Constants.java | 34 +++++---- .../andotp/Utilities/EncryptionHelper.java | 8 +++ app/src/main/res/layout/content_backup.xml | 22 ++++++ app/src/main/res/values/strings_backup.xml | 2 + 5 files changed, 108 insertions(+), 29 deletions(-) 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 a046c3f2..68d56342 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 @@ -36,7 +36,6 @@ import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.widget.Toolbar; import android.text.TextUtils; -import android.util.Log; import android.view.View; import android.view.ViewStub; import android.widget.LinearLayout; @@ -60,8 +59,10 @@ import org.shadowice.flocke.andotp.Utilities.Tools; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import javax.crypto.SecretKey; @@ -120,6 +121,7 @@ public class BackupActivity extends BaseActivity { TextView cryptSetup = v.findViewById(R.id.msg_crypt_setup); LinearLayout backupCrypt = v.findViewById(R.id.button_backup_crypt); LinearLayout restoreCrypt = v.findViewById(R.id.button_restore_crypt); + LinearLayout restoreCryptOld = v.findViewById(R.id.button_restore_crypt_old); if (settings.getBackupPasswordEnc().isEmpty()) { cryptSetup.setVisibility(View.VISIBLE); @@ -141,6 +143,13 @@ public class BackupActivity extends BaseActivity { } }); + restoreCryptOld.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD, Constants.PERMISSIONS_BACKUP_READ_IMPORT_CRYPT_OLD); + } + }); + // OpenPGP String PGPProvider = settings.getOpenPGPProvider(); @@ -231,6 +240,12 @@ public class BackupActivity extends BaseActivity { } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } + } else if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_CRYPT_OLD) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD); + } else { + Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); + } } else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { showSaveFileSelector(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BackupType.ENCRYPTED, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT); @@ -269,7 +284,11 @@ public class BackupActivity extends BaseActivity { } } else if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT && resultCode == RESULT_OK) { if (intent != null) { - doRestoreCrypt(intent.getData()); + doRestoreCrypt(intent.getData(), false); + } + } else if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD && resultCode == RESULT_OK) { + if (intent != null) { + doRestoreCrypt(intent.getData(), true); } } else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT && resultCode == RESULT_OK) { if (intent != null) { @@ -300,7 +319,9 @@ public class BackupActivity extends BaseActivity { if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN) doRestorePlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN)); else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT) - doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT)); + doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT), false); + else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD) + doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT), true); else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP) restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null); } @@ -415,34 +436,47 @@ public class BackupActivity extends BaseActivity { /* Encrypted backup functions */ - private void doRestoreCrypt(final Uri uri) { + private void doRestoreCrypt(final Uri uri, final boolean old_format) { String password = settings.getBackupPasswordEnc(); if (password.isEmpty()) { PasswordEntryDialog pwDialog = new PasswordEntryDialog(this, PasswordEntryDialog.Mode.ENTER, new PasswordEntryDialog.PasswordEnteredCallback() { @Override public void onPasswordEntered(String newPassword) { - doRestoreCryptWithPassword(uri, newPassword); + doRestoreCryptWithPassword(uri, newPassword, old_format); } }); pwDialog.show(); } else { - doRestoreCryptWithPassword(uri, password); + doRestoreCryptWithPassword(uri, password, old_format); } } - private void doRestoreCryptWithPassword(Uri uri, String password) { + private void doRestoreCryptWithPassword(Uri uri, String password, boolean old_format) { if (Tools.isExternalStorageReadable()) { boolean success = true; String decryptedString = ""; try { - byte[] encrypted = FileHelper.readFileToBytes(this, uri); + byte[] data = FileHelper.readFileToBytes(this, uri); - SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); - byte[] decrypted = EncryptionHelper.decrypt(key, encrypted); + if (old_format) { + SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); + byte[] decrypted = EncryptionHelper.decrypt(key, data); - decryptedString = new String(decrypted, StandardCharsets.UTF_8); + decryptedString = new String(decrypted, StandardCharsets.UTF_8); + } else { + byte[] iterBytes = Arrays.copyOfRange(data, 0, Constants.INT_LENGTH); + byte[] salt = Arrays.copyOfRange(data, Constants.INT_LENGTH, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH); + byte[] encrypted = Arrays.copyOfRange(data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, data.length); + + int iter = ByteBuffer.wrap(iterBytes).getInt(); + + SecretKey key = EncryptionHelper.generateSymmetricKeyPBKDF2(password, iter, salt); + + byte[] decrypted = EncryptionHelper.decrypt(key, encrypted); + decryptedString = new String(decrypted, StandardCharsets.UTF_8); + } } catch (Exception e) { success = false; e.printStackTrace(); @@ -482,10 +516,20 @@ public class BackupActivity extends BaseActivity { boolean success = true; try { - SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); + int iter = EncryptionHelper.generateRandomIterations(); + byte[] salt = EncryptionHelper.generateRandom(Constants.ENCRYPTION_IV_LENGTH); + + SecretKey key = EncryptionHelper.generateSymmetricKeyPBKDF2(password, iter, salt); byte[] encrypted = EncryptionHelper.encrypt(key, plain.getBytes(StandardCharsets.UTF_8)); - FileHelper.writeBytesToFile(this, uri, encrypted); + byte[] iterBytes = ByteBuffer.allocate(Constants.INT_LENGTH).putInt(iter).array(); + byte[] data = new byte[Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH + encrypted.length]; + + System.arraycopy(iterBytes, 0, data, 0, Constants.INT_LENGTH); + System.arraycopy(salt, 0, data, Constants.INT_LENGTH, Constants.ENCRYPTION_IV_LENGTH); + System.arraycopy(encrypted, 0, data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, encrypted.length); + + FileHelper.writeBytesToFile(this, uri, data); } catch (Exception e) { e.printStackTrace(); success = false; @@ -510,7 +554,6 @@ public class BackupActivity extends BaseActivity { decryptIntent = new Intent(OpenPgpApi.ACTION_DECRYPT_VERIFY); String input = FileHelper.readFileToString(this, uri); - Log.d("OpenPGP", input); InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream os = new ByteArrayOutputStream(); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java index 98012951..379d9432 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java @@ -58,24 +58,26 @@ public class Constants { public final static int INTENT_MAIN_BACKUP = 102; public final static int INTENT_MAIN_INTRO = 103; - public final static int INTENT_BACKUP_OPEN_DOCUMENT_PLAIN = 200; - public final static int INTENT_BACKUP_SAVE_DOCUMENT_PLAIN = 201; - public final static int INTENT_BACKUP_OPEN_DOCUMENT_CRYPT = 202; - public final static int INTENT_BACKUP_SAVE_DOCUMENT_CRYPT = 203; - public final static int INTENT_BACKUP_OPEN_DOCUMENT_PGP = 204; - public final static int INTENT_BACKUP_SAVE_DOCUMENT_PGP = 205; - public final static int INTENT_BACKUP_ENCRYPT_PGP = 206; - public final static int INTENT_BACKUP_DECRYPT_PGP = 207; + public final static int INTENT_BACKUP_OPEN_DOCUMENT_PLAIN = 200; + public final static int INTENT_BACKUP_SAVE_DOCUMENT_PLAIN = 201; + public final static int INTENT_BACKUP_OPEN_DOCUMENT_CRYPT = 202; + public final static int INTENT_BACKUP_SAVE_DOCUMENT_CRYPT = 203; + public final static int INTENT_BACKUP_OPEN_DOCUMENT_PGP = 204; + public final static int INTENT_BACKUP_SAVE_DOCUMENT_PGP = 205; + public final static int INTENT_BACKUP_ENCRYPT_PGP = 206; + public final static int INTENT_BACKUP_DECRYPT_PGP = 207; + public final static int INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD = 208; public static final int INTENT_SETTINGS_AUTHENTICATE = 300; // Permission requests (Format: A1x with A = parent Activity, x = number of the request) - public final static int PERMISSIONS_BACKUP_READ_IMPORT_PLAIN = 210; - public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN = 211; - public final static int PERMISSIONS_BACKUP_READ_IMPORT_CRYPT = 212; - public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT = 213; - public final static int PERMISSIONS_BACKUP_READ_IMPORT_PGP = 214; - public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_PGP = 215; + public final static int PERMISSIONS_BACKUP_READ_IMPORT_PLAIN = 210; + public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN = 211; + public final static int PERMISSIONS_BACKUP_READ_IMPORT_CRYPT = 212; + public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT = 213; + public final static int PERMISSIONS_BACKUP_READ_IMPORT_PGP = 214; + public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_PGP = 215; + public final static int PERMISSIONS_BACKUP_READ_IMPORT_CRYPT_OLD = 216; // Intent extras public final static String EXTRA_AUTH_PASSWORD_KEY = "password_key"; @@ -92,7 +94,9 @@ public class Constants { final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; final static int ENCRYPTION_KEY_LENGTH = 16; // 128-bit encryption key (KeyStore-mode) - final static int ENCRYPTION_IV_LENGTH = 12; + public final static int ENCRYPTION_IV_LENGTH = 12; + + public final static int INT_LENGTH = 4; final static int PBKDF2_MIN_ITERATIONS = 1000; final static int PBKDF2_MAX_ITERATIONS = 5000; 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 81c3209c..229309a7 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 @@ -89,6 +89,14 @@ public class EncryptionHelper { return new SecretKeySpec(data, 0, data.length, "AES"); } + public static SecretKey generateSymmetricKeyPBKDF2(String password, int iter, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iter, Constants.PBKDF2_LENGTH); + + return secretKeyFactory.generateSecret(keySpec); + } + public static SecretKey generateSymmetricKeyFromPassword(String password) throws NoSuchAlgorithmException { MessageDigest sha = MessageDigest.getInstance("SHA-256"); diff --git a/app/src/main/res/layout/content_backup.xml b/app/src/main/res/layout/content_backup.xml index 48a22040..90cfe05a 100644 --- a/app/src/main/res/layout/content_backup.xml +++ b/app/src/main/res/layout/content_backup.xml @@ -129,6 +129,28 @@ + + + + + + + + Backup (OpenPGP) Restore (plain-text) Restore (encrypted) + Restore (encrypted, old encryption) Restore (OpenPGP) Replace existing entries @@ -20,6 +21,7 @@ Backup all accounts in an OpenPGP-encrypted JSON file Restore accounts from a plain-text JSON file Restore accounts from a password-protected JSON file + Restore accounts from a password-protected JSON file using the old, insecure encryption Restore accounts from an OpenPGP-encrypted JSON file Failed to load the backup password from the Settings,