Merge pull request #647 from alexbakker/more-tests

Add some more tests
This commit is contained in:
Alexander Bakker 2021-01-16 15:39:02 +01:00 committed by GitHub
commit a5ec7666ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 974 additions and 321 deletions

View file

@ -4,11 +4,13 @@ import android.content.Context;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
@ -18,6 +20,7 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class AegisImporter extends DatabaseImporter {
@ -56,11 +59,24 @@ public class AegisImporter extends DatabaseImporter {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
JSONObject obj = _file.getContent(creds);
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
JSONObject obj;
try {
obj = _file.getContent(creds);
} catch (VaultFileException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(obj);
}
public State decrypt(char[] password) throws DatabaseImporterException {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
return decrypt(creds);
}
@Override
public void decrypt(Context context, DecryptListener listener) {

View file

@ -95,7 +95,7 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
private DecryptedState decryptContent(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);
@ -116,35 +116,52 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
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, INT_SIZE + SALT_SIZE);
return new KeyDerivationParams(password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
// 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");
return decryptContent(key, 0);
}
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
return decryptContent(key, INT_SIZE + SALT_SIZE);
}
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
KeyDerivationParams params = getKeyDerivationParams(password);
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
return decryptNewFormat(key);
}
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);
DecryptedState state = decryptOldFormat(password);
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 -> {
KeyDerivationParams params = getKeyDerivationParams(password);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
try {
DecryptedState state = decryptData(key1, offset);
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
@ -251,7 +268,7 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
@ -260,20 +277,22 @@ public class AndOtpImporter extends DatabaseImporter {
}
@Override
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
setPriority();
AndOtpKeyDerivationTask.Params params = args[0];
SecretKey key;
AndOtpImporter.KeyDerivationParams params = args[0];
return deriveKey(params);
}
protected static SecretKey deriveKey(KeyDerivationParams params) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
key = factory.generateSecret(spec);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return key;
}
@Override
@ -282,32 +301,32 @@ public class AndOtpImporter extends DatabaseImporter {
_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);
}
}
protected static class KeyDerivationParams {
private final char[] _password;
private final byte[] _salt;
private final int _iterations;
public KeyDerivationParams(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;
}
}
}

View file

@ -37,31 +37,38 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
private final byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected State decrypt(char[] password) throws DatabaseImporterException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
return importer.read(zipStream);
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
DatabaseImporter.State state = importer.read(zipStream);
listener.onStateDecrypted(state);
return;
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException | DatabaseImporterException e) {
try {
DatabaseImporter.State state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});

View file

@ -42,6 +42,7 @@ import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs";
@ -154,44 +155,53 @@ public class AuthyImporter extends DatabaseImporter {
_array = array;
}
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
key = new SecretKeySpec(key.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
return new DecryptedState(_array);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
DecryptedState state = new DecryptedState(_array);
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
@ -274,6 +284,10 @@ public class AuthyImporter extends DatabaseImporter {
info.Issuer = info.Name;
info.Name = "";
}
if (info.Name.startsWith(": ")) {
info.Name = info.Name.substring(2);
}
}
}

View file

@ -28,9 +28,9 @@ public abstract class DatabaseImporter {
// note: keep these lists sorted alphabetically
_importers = new ArrayList<>();
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));

View file

@ -115,30 +115,6 @@ public class FreeOtpImporter extends DatabaseImporter {
}
}
private static List<JSONObject> parseXml(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
List<JSONObject> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
JSONObject entry = parseXmlEntry(parser);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
@ -146,46 +122,4 @@ public class FreeOtpImporter extends DatabaseImporter {
}
return bytes;
}
private static JSONObject parseXmlEntry(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseXmlText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
if (name.equals("tokenOrder")) {
return null;
}
return new JSONObject(value);
}
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}

View file

@ -104,8 +104,8 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
String s = array.getString(i);
entries.add(new JSONObject(s));
JSONObject obj = array.getJSONObject(i);
entries.add(obj);
}
return entries;
@ -119,7 +119,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is not a secure way to derive a key from a password
@ -127,7 +127,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);

View file

@ -34,6 +34,15 @@ public class TotpInfo extends OtpInfo {
}
}
public String getOtp(long time) {
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
return ID;

View file

@ -619,7 +619,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
DatabaseImporter.State state;
try {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (VaultFileException e) {
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
return;

View file

@ -28,9 +28,13 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
setPriority();
Params params = args[0];
for (PasswordSlot slot : params.getSlots()) {
return decrypt(params.getSlots(), params.getPassword());
}
public static Result decrypt(List<PasswordSlot> slots, char[] password) {
for (PasswordSlot slot : slots) {
try {
return decryptPasswordSlot(slot, params.getPassword());
return decryptPasswordSlot(slot, password);
} catch (SlotException e) {
throw new RuntimeException(e);
} catch (SlotIntegrityException ignored) {
@ -41,7 +45,7 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
return null;
}
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
public static Result decryptPasswordSlot(PasswordSlot slot, char[] password)
throws SlotIntegrityException, SlotException {
MasterKey masterKey;
SecretKey key = slot.deriveKey(password);
@ -56,8 +60,6 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
throw e;
}
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
// try to decrypt the password slot with the old key
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
masterKey = decryptPasswordSlot(slot, oldKey);
@ -75,7 +77,7 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
return new Result(masterKey, slot, repaired);
}
private MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
public static MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
throws SlotException, SlotIntegrityException {
Cipher cipher = slot.createDecryptCipher(key);
return slot.getKey(cipher);

View file

@ -70,6 +70,10 @@ public class PasswordSlot extends RawSlot {
return _repaired;
}
public SCryptParameters getSCryptParameters() {
return _params;
}
@Override
public byte getType() {
return TYPE_DERIVED;

View file

@ -51,7 +51,7 @@ public abstract class Slot extends UUIDMap.Value {
public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException {
try {
CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams);
SecretKey key = new SecretKeySpec(res.getData(), CryptoUtils.CRYPTO_AEAD);
SecretKey key = new SecretKeySpec(res.getData(), "AES");
return new MasterKey(key);
} catch (BadPaddingException e) {
throw new SlotIntegrityException(e);
@ -97,6 +97,10 @@ public abstract class Slot extends UUIDMap.Value {
}
}
protected byte[] getEncryptedMasterKey() {
return _encryptedMasterKey;
}
public JSONObject toJson() {
try {
JSONObject obj = new JSONObject();
@ -130,10 +134,10 @@ public abstract class Slot extends UUIDMap.Value {
break;
case Slot.TYPE_DERIVED:
SCryptParameters scryptParams = new SCryptParameters(
obj.getInt("n"),
obj.getInt("r"),
obj.getInt("p"),
Hex.decode(obj.getString("salt"))
obj.getInt("n"),
obj.getInt("r"),
obj.getInt("p"),
Hex.decode(obj.getString("salt"))
);
boolean repaired = obj.optBoolean("repaired", false);
slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired);