diff --git a/app/build.gradle b/app/build.gradle
index 506e34d549..a55c61f0c7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
- versionCode 21
- versionName "1.0.2"
+ versionCode 22
+ versionName "1.0.3"
}
lintOptions {
diff --git a/app/src/main/assets/changelog.html b/app/src/main/assets/changelog.html
index ea72fbc3ae..f9070eddfc 100644
--- a/app/src/main/assets/changelog.html
+++ b/app/src/main/assets/changelog.html
@@ -31,6 +31,11 @@
+Version 1.0.3
+New
+
+ - Support for andOTP's new backup file format
+
Version 1.0.2
Fixes
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 4635fbb50c..ee1dac84ae 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 @@
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.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.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 EncryptedState(byte[] data) {
_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 DecryptedState decrypt(char[] password) throws DatabaseImporterException
@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 d0652adf76..5fcb672e0c 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