From b1d9be10f21587c547ddf9efaf853be421df3975 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Fri, 20 Sep 2019 22:35:30 +0200 Subject: [PATCH] Add support for the new andOTP backup format This patch adds support for the new backup file format of andOTP. andOTP has improved their security by switching from SHA-256 to PBKDF2 to derive the key for encrypted backups. Glad to see this has been addressed now. Awesome work, @flocke! See: andOTP/andOTP@d96b037. (cherry picked from commit a5165e2345e5bc4cd918c011e006229d9e96f51e) --- .../aegis/importers/AndOtpImporter.java | 85 ++++++++++++++----- app/src/main/res/values/strings.xml | 3 + 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java index 4635fbb5..ee1dac84 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java @@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; +import androidx.appcompat.app.AlertDialog; + +import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.crypto.CryptParameters; import com.beemdevelopment.aegis.crypto.CryptResult; import com.beemdevelopment.aegis.crypto.CryptoUtils; @@ -20,11 +23,14 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; import java.util.Arrays; import javax.crypto.BadPaddingException; @@ -32,9 +38,18 @@ import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class AndOtpImporter extends DatabaseImporter { + private static final int INT_SIZE = 4; + private static final int NONCE_SIZE = 12; + private static final int TAG_SIZE = 16; + private static final int SALT_SIZE = 12; + private static final int KEY_SIZE = 256; // bits + + private static final int MAX_ITERATIONS = 10000; public AndOtpImporter(Context context) { super(context); @@ -81,30 +96,48 @@ public class AndOtpImporter extends DatabaseImporter { _data = data; } - public DecryptedState decrypt(char[] password) throws DatabaseImporterException { + private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException { try { - // WARNING: DON'T DO THIS IN YOUR OWN CODE - // this exists solely to support encrypted andOTP backups - // it is not a secure way to derive a key from a password - MessageDigest hash = MessageDigest.getInstance("SHA-256"); - byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password)); - SecretKey key = new SecretKeySpec(keyBytes, "AES"); + SecretKey key; + int offset = 0; + + if (oldFormat) { + // WARNING: DON'T DO THIS IN YOUR OWN CODE + // this exists solely to support the old andOTP backup format + // it is not a secure way to derive a key from a password + MessageDigest hash = MessageDigest.getInstance("SHA-256"); + byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password)); + key = new SecretKeySpec(keyBytes, "AES"); + } else { + offset = INT_SIZE + SALT_SIZE; + + byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE); + int iterations = ByteBuffer.wrap(iterBytes).getInt(); + if (iterations < 1 || iterations > MAX_ITERATIONS) { + throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations)); + } + + byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE); + key = factory.generateSecret(spec); + } // extract nonce and tag - byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE); - byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length); + byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE); + byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length); CryptParameters params = new CryptParameters(nonce, tag); Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce); - int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE; - int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE; - CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params); + int len = _data.length - offset - NONCE_SIZE - TAG_SIZE; + CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params); return read(result.getData()); } catch (IOException | BadPaddingException | JSONException e) { throw new DatabaseImporterException(e); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException + | InvalidKeySpecException | NoSuchPaddingException | IllegalBlockSizeException e) { throw new RuntimeException(e); @@ -113,14 +146,26 @@ public class AndOtpImporter extends DatabaseImporter { @Override public void decrypt(Context context, DecryptListener listener) { - Dialogs.showPasswordInputDialog(context, password -> { - try { - DecryptedState state = decrypt(password); - listener.onStateDecrypted(state); - } catch (DatabaseImporterException e) { - listener.onError(e); - } - }); + String[] choices = new String[]{ + context.getResources().getString(R.string.andotp_new_format), + context.getResources().getString(R.string.andotp_old_format) + }; + + Dialogs.showSecureDialog(new AlertDialog.Builder(context) + .setTitle(R.string.choose_andotp_importer) + .setSingleChoiceItems(choices, 0, null) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + Dialogs.showPasswordInputDialog(context, password -> { + try { + DecryptedState state = decrypt(password, i != 0); + listener.onStateDecrypted(state); + } catch (DatabaseImporterException e) { + listener.onError(e); + } + }); + }) + .create()); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d0652adf..5fcb672e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,9 @@ An error occurred while enabling encryption An error occurred while disabling encryption Permission denied + New format (v0.6.3 or newer) + Old format (v0.6.2 or older) + Which format does the andOTP backup file have? Select the application you\'d like to import a database from Select your desired theme Select your desired view mode