mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-15 22:42:51 +00:00
Add support for importing from TOTP Authenticator
This is by far the most ridiculous format/'security' I've seen so far.
This commit is contained in:
parent
cd42b0dc65
commit
7e7c5317b1
5 changed files with 251 additions and 0 deletions
|
@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.encoding;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class Base64 {
|
public class Base64 {
|
||||||
private Base64() {
|
private Base64() {
|
||||||
|
|
||||||
|
@ -15,6 +17,10 @@ public class Base64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] decode(byte[] s) throws EncodingException {
|
||||||
|
return decode(new String(s, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
public static String encode(byte[] data) {
|
public static String encode(byte[] data) {
|
||||||
return BaseEncoding.base64().encode(data);
|
return BaseEncoding.base64().encode(data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ public abstract class DatabaseImporter {
|
||||||
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
|
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||||
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
||||||
_importers.put("Steam", SteamImporter.class);
|
_importers.put("Steam", SteamImporter.class);
|
||||||
|
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
|
||||||
_importers.put("WinAuth", WinAuthImporter.class);
|
_importers.put("WinAuth", WinAuthImporter.class);
|
||||||
|
|
||||||
_appImporters = new LinkedHashMap<>();
|
_appImporters = new LinkedHashMap<>();
|
||||||
|
@ -41,6 +42,7 @@ public abstract class DatabaseImporter {
|
||||||
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
|
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||||
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
||||||
_appImporters.put("Steam", SteamImporter.class);
|
_appImporters.put("Steam", SteamImporter.class);
|
||||||
|
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DatabaseImporter(Context context) {
|
public DatabaseImporter(Context context) {
|
||||||
|
|
|
@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
|
||||||
public class DatabaseImporterEntryException extends Exception {
|
public class DatabaseImporterEntryException extends Exception {
|
||||||
private String _text;
|
private String _text;
|
||||||
|
|
||||||
|
public DatabaseImporterEntryException(String message, String text) {
|
||||||
|
super(message);
|
||||||
|
_text = text;
|
||||||
|
}
|
||||||
|
|
||||||
public DatabaseImporterEntryException(Throwable cause, String text) {
|
public DatabaseImporterEntryException(Throwable cause, String text) {
|
||||||
super(cause);
|
super(cause);
|
||||||
_text = text;
|
_text = text;
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
package com.beemdevelopment.aegis.importers;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Xml;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.R;
|
||||||
|
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Base32;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Base64;
|
||||||
|
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
|
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||||
|
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||||
|
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||||
|
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||||
|
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
|
||||||
|
private static final String _pkgName = "com.authenticator.authservice2";
|
||||||
|
|
||||||
|
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||||
|
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
|
||||||
|
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
|
||||||
|
private static final byte[] IV = new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
|
||||||
|
|
||||||
|
public TotpAuthenticatorImporter(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getAppPkgName() {
|
||||||
|
return _pkgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getAppSubPath() {
|
||||||
|
return _subPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State read(FileReader reader) throws DatabaseImporterException {
|
||||||
|
try {
|
||||||
|
if (reader.isInternal()) {
|
||||||
|
XmlPullParser parser = Xml.newPullParser();
|
||||||
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||||
|
parser.setInput(reader.getStream(), null);
|
||||||
|
parser.nextTag();
|
||||||
|
|
||||||
|
String data = null;
|
||||||
|
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||||
|
if (entry.Name.equals(PREF_KEY)) {
|
||||||
|
data = entry.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<JSONObject> entries = parse(data);
|
||||||
|
return new DecryptedState(entries);
|
||||||
|
} else {
|
||||||
|
byte[] base64 = reader.readAll();
|
||||||
|
byte[] cipherText = Base64.decode(base64);
|
||||||
|
return new EncryptedState(cipherText);
|
||||||
|
}
|
||||||
|
} catch (IOException | XmlPullParserException | JSONException e) {
|
||||||
|
throw new DatabaseImporterException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<JSONObject> parse(String data) throws JSONException {
|
||||||
|
JSONArray array = new JSONArray(data);
|
||||||
|
|
||||||
|
List<JSONObject> entries = new ArrayList<>();
|
||||||
|
for (int i = 0; i < array.length(); ++i) {
|
||||||
|
String s = array.getString(i);
|
||||||
|
entries.add(new JSONObject(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EncryptedState extends DatabaseImporter.State {
|
||||||
|
private byte[] _data;
|
||||||
|
|
||||||
|
public EncryptedState(byte[] data) {
|
||||||
|
super(true);
|
||||||
|
_data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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
|
||||||
|
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||||
|
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||||
|
IvParameterSpec spec = new IvParameterSpec(IV);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||||
|
|
||||||
|
byte[] bytes = cipher.doFinal(_data);
|
||||||
|
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||||
|
JSONArray keys = obj.names();
|
||||||
|
|
||||||
|
List<JSONObject> entries = new ArrayList<>();
|
||||||
|
if (keys != null && keys.length() > 0) {
|
||||||
|
entries = parse((String) keys.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DecryptedState(entries);
|
||||||
|
} catch (NoSuchAlgorithmException
|
||||||
|
| NoSuchPaddingException
|
||||||
|
| InvalidAlgorithmParameterException
|
||||||
|
| InvalidKeyException
|
||||||
|
| BadPaddingException
|
||||||
|
| IllegalBlockSizeException
|
||||||
|
| JSONException e) {
|
||||||
|
throw new DatabaseImporterException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void decrypt(Context context, DecryptListener listener) {
|
||||||
|
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||||
|
.setMessage(R.string.choose_totpauth_importer)
|
||||||
|
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||||
|
Dialogs.showPasswordInputDialog(context, password -> {
|
||||||
|
decrypt(password, listener);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||||
|
decrypt(PASSWORD, listener);
|
||||||
|
})
|
||||||
|
.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decrypt(char[] password, DecryptListener listener) {
|
||||||
|
try {
|
||||||
|
DecryptedState state = decrypt(password);
|
||||||
|
listener.onStateDecrypted(state);
|
||||||
|
} catch (DatabaseImporterException e) {
|
||||||
|
listener.onError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DecryptedState extends DatabaseImporter.State {
|
||||||
|
private List<JSONObject> _objs;
|
||||||
|
|
||||||
|
private DecryptedState(List<JSONObject> objs) {
|
||||||
|
super(false);
|
||||||
|
_objs = objs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result convert() {
|
||||||
|
Result result = new Result();
|
||||||
|
|
||||||
|
for (JSONObject obj : _objs) {
|
||||||
|
try {
|
||||||
|
VaultEntry entry = convertEntry(obj);
|
||||||
|
result.addEntry(entry);
|
||||||
|
} catch (DatabaseImporterEntryException e) {
|
||||||
|
result.addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||||
|
try {
|
||||||
|
int base = obj.getInt("base");
|
||||||
|
String secretString = obj.getString("key");
|
||||||
|
|
||||||
|
byte[] secret;
|
||||||
|
switch (base) {
|
||||||
|
case 16:
|
||||||
|
secret = Hex.decode(secretString);
|
||||||
|
break;
|
||||||
|
case 32:
|
||||||
|
secret = Base32.decode(secretString);
|
||||||
|
break;
|
||||||
|
case 64:
|
||||||
|
secret = Base64.decode(secretString);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
|
||||||
|
String name = obj.optString("name");
|
||||||
|
String issuer = obj.optString("issuer");
|
||||||
|
|
||||||
|
return new VaultEntry(info, name, issuer);
|
||||||
|
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||||
|
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,6 +67,8 @@
|
||||||
<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="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
<string name="unlock">Unlock</string>
|
<string name="unlock">Unlock</string>
|
||||||
<string name="biometrics">Biometrics</string>
|
<string name="biometrics">Biometrics</string>
|
||||||
<string name="advanced">Advanced</string>
|
<string name="advanced">Advanced</string>
|
||||||
|
@ -122,6 +124,7 @@
|
||||||
<string name="andotp_new_format">New format (v0.6.3 or newer) </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="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_andotp_importer">Which format does the andOTP backup file have?</string>
|
||||||
|
<string name="choose_totpauth_importer">Is this TOTP Authenticator backup encrypted with a password?</string>
|
||||||
<string name="choose_application">Select the application you\'d like to import from</string>
|
<string name="choose_application">Select the application you\'d like to import 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
Add a link
Reference in a new issue