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 a5165e2345)
This commit is contained in:
Alexander Bakker 2019-09-20 22:35:30 +02:00
parent 1d513441c6
commit b1d9be10f2
2 changed files with 68 additions and 20 deletions

View file

@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context; 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.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult; import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils; import com.beemdevelopment.aegis.crypto.CryptoUtils;
@ -20,11 +23,14 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays; import java.util.Arrays;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
@ -32,9 +38,18 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter { 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) { public AndOtpImporter(Context context) {
super(context); super(context);
@ -81,30 +96,48 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data; _data = data;
} }
public DecryptedState decrypt(char[] password) throws DatabaseImporterException { private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
try { try {
SecretKey key;
int offset = 0;
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE // WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support encrypted andOTP backups // this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password // it is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256"); MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password)); byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES"); 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 // extract nonce and tag
byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE); byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length); byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag); CryptParameters params = new CryptParameters(nonce, tag);
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce); Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE; int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE; CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params);
return read(result.getData()); return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) { } catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException } catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException | InvalidAlgorithmParameterException
| InvalidKeyException | InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException | NoSuchPaddingException
| IllegalBlockSizeException e) { | IllegalBlockSizeException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -113,14 +146,26 @@ public class AndOtpImporter extends DatabaseImporter {
@Override @Override
public void decrypt(Context context, DecryptListener listener) { public void decrypt(Context context, DecryptListener listener) {
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 -> { Dialogs.showPasswordInputDialog(context, password -> {
try { try {
DecryptedState state = decrypt(password); DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state); listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) { } catch (DatabaseImporterException e) {
listener.onError(e); listener.onError(e);
} }
}); });
})
.create());
} }
} }

View file

@ -111,6 +111,9 @@
<string name="enable_encryption_error">An error occurred while enabling encryption</string> <string name="enable_encryption_error">An error occurred while enabling encryption</string>
<string name="disable_encryption_error">An error occurred while disabling encryption</string> <string name="disable_encryption_error">An error occurred while disabling encryption</string>
<string name="permission_denied">Permission denied</string> <string name="permission_denied">Permission denied</string>
<string name="andotp_new_format">New format (v0.6.3 or newer) </string>
<string name="andotp_old_format">Old format (v0.6.2 or older) </string>
<string name="choose_andotp_importer">Which format does the andOTP backup file have?</string>
<string name="choose_application">Select the application you\'d like to import a database from</string> <string name="choose_application">Select the application you\'d like to import a database from</string>
<string name="choose_theme">Select your desired theme</string> <string name="choose_theme">Select your desired theme</string>
<string name="choose_view_mode">Select your desired view mode</string> <string name="choose_view_mode">Select your desired view mode</string>