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.
This commit is contained in:
parent
2f77fe4644
commit
d96b0373d9
5 changed files with 108 additions and 29 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -129,6 +129,28 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_restore_crypt_old"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/activity_margin"
|
||||
android:background="?android:attr/selectableItemBackground" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:text="@string/backup_title_import_crypt_old" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="@string/backup_desc_import_crypt_old"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<string name="backup_title_export_openpgp">Backup (OpenPGP)</string>
|
||||
<string name="backup_title_import_plain">Restore (plain-text)</string>
|
||||
<string name="backup_title_import_crypt">Restore (encrypted)</string>
|
||||
<string name="backup_title_import_crypt_old">Restore (encrypted, old encryption)</string>
|
||||
<string name="backup_title_import_openpgp">Restore (OpenPGP)</string>
|
||||
|
||||
<string name="backup_title_replace">Replace existing entries</string>
|
||||
|
@ -20,6 +21,7 @@
|
|||
<string name="backup_desc_export_openpgp">Backup all accounts in an OpenPGP-encrypted JSON file</string>
|
||||
<string name="backup_desc_import_plain">Restore accounts from a plain-text JSON file</string>
|
||||
<string name="backup_desc_import_crypt">Restore accounts from a password-protected JSON file</string>
|
||||
<string name="backup_desc_import_crypt_old">Restore accounts from a password-protected JSON file using the old, insecure encryption</string>
|
||||
<string name="backup_desc_import_openpgp">Restore accounts from an OpenPGP-encrypted JSON file</string>
|
||||
|
||||
<string name="backup_desc_crypt_setup">Failed to load the backup password from the <b>Settings</b>,
|
||||
|
|
Loading…
Reference in a new issue