Add support for importing encrypted and manually entered tokens from Authy

This commit is contained in:
Alexander Bakker 2020-02-01 20:52:14 +01:00
parent cd42b0dc65
commit 2a4bb902df
3 changed files with 119 additions and 20 deletions

View file

@ -3,13 +3,16 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context; import android.content.Context;
import android.util.Xml; import android.util.Xml;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException; import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.PreferenceParser; import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -18,11 +21,33 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
public class AuthyImporter extends DatabaseImporter { public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml"; private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _pkgName = "com.authy.authy"; private static final String _pkgName = "com.authy.authy";
private static final int ITERATIONS = 1000;
private static final int KEY_SIZE = 256;
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
public AuthyImporter(Context context) { public AuthyImporter(Context context) {
super(context); super(context);
} }
@ -52,18 +77,76 @@ public class AuthyImporter extends DatabaseImporter {
} }
} }
return new State(array); for (int i = 0; i < array.length(); i++) {
if (!array.getJSONObject(i).has("decryptedSecret")) {
return new EncryptedState(array);
}
}
return new DecryptedState(array);
} catch (XmlPullParserException | JSONException | IOException e) { } catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);
} }
} }
public static class State extends DatabaseImporter.State { public static class EncryptedState extends DatabaseImporter.State {
private JSONArray _obj; private JSONArray _array;
private State(JSONArray obj) { private EncryptedState(JSONArray array) {
super(true);
_array = array;
}
@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 = obj.optString("encryptedSecret", null);
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);
listener.onStateDecrypted(state);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
listener.onError(e);
}
});
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _array;
private DecryptedState(JSONArray array) {
super(false); super(false);
_obj = obj; _array = array;
} }
@Override @Override
@ -71,8 +154,8 @@ public class AuthyImporter extends DatabaseImporter {
Result result = new Result(); Result result = new Result();
try { try {
for (int i = 0; i < _obj.length(); i++) { for (int i = 0; i < _array.length(); i++) {
JSONObject entryObj = _obj.getJSONObject(i); JSONObject entryObj = _array.getJSONObject(i);
try { try {
VaultEntry entry = convertEntry(entryObj); VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry); result.addEntry(entry);
@ -90,7 +173,8 @@ public class AuthyImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException { private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try { try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo(); AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.getString("originalName"); authyEntryInfo.OriginalName = entry.optString("originalName", null);
authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
authyEntryInfo.AccountType = entry.getString("accountType"); authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.Name = entry.optString("name"); authyEntryInfo.Name = entry.optString("name");
@ -108,24 +192,27 @@ public class AuthyImporter extends DatabaseImporter {
} }
private static void sanitizeEntryInfo(AuthyEntryInfo info) { private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String seperator = ""; String separator = "";
if (info.OriginalName.contains(":")) { if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":")); info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
seperator = ":"; separator = ":";
} else if (info.Name.contains(" - ")) { } else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - ")); info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
seperator = " - "; separator = " - ";
} else { } else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1); info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
} }
info.Name = info.Name.replace(info.Issuer + seperator, ""); info.Name = info.Name.replace(info.Issuer + separator, "");
} }
} }
private static class AuthyEntryInfo { private static class AuthyEntryInfo {
String OriginalName; String OriginalName;
String OriginalIssuer;
String AccountType; String AccountType;
String Issuer; String Issuer;
String Name; String Name;

View file

@ -156,7 +156,7 @@ public class Dialogs {
showSecureDialog(dialog); showSecureDialog(dialog);
} }
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null); View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input); EditText input = view.findViewById(R.id.text_input);
if (isSecret) { if (isSecret) {
@ -164,18 +164,25 @@ public class Dialogs {
} }
input.setHint(hintId); input.setHint(hintId);
AlertDialog dialog = new AlertDialog.Builder(context) AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId) .setTitle(titleId)
.setView(view) .setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> { .setPositiveButton(android.R.string.ok, (dialog1, which) -> {
char[] text = EditTextHelper.getEditTextChars(input); char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text); listener.onTextInputResult(text);
}) });
.create(); if (messageId != 0) {
builder.setMessage(messageId);
}
AlertDialog dialog = builder.create();
showSecureDialog(dialog); showSecureDialog(dialog);
} }
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) { public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false); showTextInputDialog(context, titleId, hintId, listener, false);
} }
@ -184,6 +191,10 @@ public class Dialogs {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true); showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
} }
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true);
}
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) { public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null); View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker); NumberPicker numberPicker = view.findViewById(R.id.numberPicker);

View file

@ -66,6 +66,7 @@
<string name="set_password_confirm">Please confirm the password</string> <string name="set_password_confirm">Please confirm the password</string>
<string name="invalidated_biometrics">A change in your device\'s security settings has been detected. Please go to \"Aegis -> Settings -> Biometrics\" and re-enable biometric unlock.</string> <string name="invalidated_biometrics">A change in your device\'s security settings has been detected. Please go to \"Aegis -> Settings -> Biometrics\" and re-enable biometric unlock.</string>
<string name="password_reminder">It\'s been a while since you\'ve entered your password. Do you still remember it?</string> <string name="password_reminder">It\'s been a while since you\'ve entered your password. Do you still remember it?</string>
<string name="enter_password_authy_message">It looks like your Authy tokens are encrypted. Please close Aegis, open Authy and unlock the tokens with your password. Instead, Aegis can also attempt to decrypt your Authy tokens for you, if you enter your password below.</string>
<string name="unlock">Unlock</string> <string name="unlock">Unlock</string>
<string name="biometrics">Biometrics</string> <string name="biometrics">Biometrics</string>