mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-24 07:46:07 +00:00
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:
parent
1d513441c6
commit
b1d9be10f2
2 changed files with 68 additions and 20 deletions
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue