Merge pull request #454 from alexbakker/fix-andotp-iters

Fix importing andOTP backups with more than 10000 PBKDF iterations
This commit is contained in:
Michael Schättgen 2020-06-06 13:50:33 +02:00 committed by GitHub
commit 8e5b51c42f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 118 additions and 46 deletions

View file

@ -3,21 +3,19 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
public class Base32 {
private static final BaseEncoding _encoding = BaseEncoding.base32().omitPadding();
private Base32() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return _encoding.decode(s.toUpperCase());
return BaseEncoding.base32().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return _encoding.encode(data);
return BaseEncoding.base32().omitPadding().encode(data);
}
}

View file

@ -8,7 +8,6 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
@ -17,6 +16,8 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.json.JSONArray;
import org.json.JSONException;
@ -49,8 +50,6 @@ public class AndOtpImporter extends DatabaseImporter {
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);
}
@ -96,38 +95,12 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
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);
try {
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, 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 len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
@ -137,13 +110,50 @@ public class AndOtpImporter extends DatabaseImporter {
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
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;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
DecryptedState state = decryptData(key, 0);
listener.onStateDecrypted(state);
} else {
int offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
try {
DecryptedState state = decryptData(key1, offset);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
task.execute(params);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
@ -158,8 +168,7 @@ public class AndOtpImporter extends DatabaseImporter {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state);
decrypt(context, password, i != 0, listener);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
@ -221,12 +230,17 @@ public class AndOtpImporter extends DatabaseImporter {
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
name = parts[0];
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
@ -235,4 +249,64 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
}
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
setPriority();
AndOtpKeyDerivationTask.Params params = args[0];
SecretKey key;
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
key = factory.generateSecret(spec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return key;
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public static class Params {
private char[] _password;
private byte[] _salt;
private int _iterations;
public Params(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
}