diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ede274d..debc5516 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -154,6 +154,7 @@ + diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/BattleNetImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/BattleNetImporter.java new file mode 100644 index 00000000..8ff50ff4 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/BattleNetImporter.java @@ -0,0 +1,116 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.Xml; + +import com.beemdevelopment.aegis.encoding.EncodingException; +import com.beemdevelopment.aegis.encoding.Hex; +import com.beemdevelopment.aegis.otp.OtpInfo; +import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.otp.TotpInfo; +import com.beemdevelopment.aegis.util.PreferenceParser; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.topjohnwu.superuser.io.SuFile; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class BattleNetImporter extends DatabaseImporter { + private static final String _pkgName = "com.blizzard.bma"; + private static final String _subPath = "shared_prefs/com.blizzard.bma.AUTH_STORE.xml"; + + private static final byte[] _key; + + public BattleNetImporter(Context context) { + super(context); + } + + static { + try { + _key = Hex.decode("398e27fc50276a656065b0e525f4c06c04c61075286b8e7aeda59da9813b5dd6c80d2fb38068773fa59ba47c17ca6c6479015c1d5b8b8f6b9a"); + } catch (EncodingException e) { + throw new RuntimeException(e); + } + } + + @Override + protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException { + return getAppPath(_pkgName, _subPath); + } + + @Override + protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException { + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(stream, null); + parser.nextTag(); + + List entries = new ArrayList<>(); + for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) { + if (entry.Name.equals("com.blizzard.bma.AUTH_STORE.HASH")) { + entries.add(entry.Value); + break; + } + } + return new BattleNetImporter.State(entries); + } catch (XmlPullParserException | IOException e) { + throw new DatabaseImporterException(e); + } + } + + public static class State extends DatabaseImporter.State { + private final List _entries; + + public State(List entries) { + super(false); + _entries = entries; + } + + @Override + public Result convert() { + Result result = new Result(); + + for (String str : _entries) { + try { + VaultEntry entry = convertEntry(str); + result.addEntry(entry); + } catch (DatabaseImporterEntryException e) { + result.addError(e); + } + } + + return result; + } + + private static VaultEntry convertEntry(String hashString) throws DatabaseImporterEntryException { + try { + byte[] hash = Hex.decode(hashString); + if (hash.length != _key.length) { + throw new DatabaseImporterEntryException(String.format("Unexpected hash length: %d", hash.length), hashString); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hash.length; i++) { + char c = (char) (hash[i] ^ _key[i]); + sb.append(c); + } + + final int secretLen = 40; + byte[] secret = Hex.decode(sb.substring(0, secretLen)); + String serial = sb.substring(secretLen); + + OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, 8, TotpInfo.DEFAULT_PERIOD); + return new VaultEntry(info, serial, "Battle.net"); + } catch (OtpInfoException | EncodingException e) { + throw new DatabaseImporterEntryException(e, hashString); + } + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java index 494ab42f..90363b0c 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -34,6 +34,7 @@ public abstract class DatabaseImporter { _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("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true)); _importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false)); _importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true)); _importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true)); @@ -105,6 +106,13 @@ public abstract class DatabaseImporter { private final @StringRes int _help; private final boolean _supportsDirect; + /** + * + * @param name The name of the Authenticator the importer handles. + * @param type The class which does the importing. + * @param help The string that explains the type of file needed (and optionally where it can be obtained). + * @param supportsDirect Whether the importer can directly import the entries from the app's internal storage using root access. + */ public Definition(String name, Class type, @StringRes int help, boolean supportsDirect) { _name = name; _type = type; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 486f0572..dfd08c9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,6 +456,7 @@ Supply a copy of /data/data/com.authy.authy/shared_prefs/com.authy.storage.tokens.authenticator.xml, located in the internal storage directory of Authy. Supply an andOTP export/backup file. Supply a Bitwarden export/backup file. Encrypted files are not supported. + Supply a copy of /data/data/com.blizzard.bma/shared_prefs/com.blizzard.bma.AUTH_STORE.xml, located in the internal storage directory of Battle.net Authenticator. Supply a copy of /data/data/com.duosecurity.duomobile/files/duokit/accounts.json, located in the internal storage directory of DUO. Supply a copy of /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml, located in the internal storage directory of FreeOTP. Supply a FreeOTP+ export file. diff --git a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java index 2fc3d89d..9b00db22 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -158,6 +158,15 @@ public class DatabaseImporterTest { checkImportedAuthyEntries(entries); } + @Test + public void testImportBattleNetXml() throws DatabaseImporterException, IOException, OtpInfoException { + List entries = importPlain(BattleNetImporter.class, "battle_net_authenticator.xml"); + + for (VaultEntry entry : entries) { + checkImportedEntry(entry); + } + } + @Test public void testImportBitwardenJson() throws IOException, DatabaseImporterException, OtpInfoException { List entries = importPlain(BitwardenImporter.class, "bitwarden.json"); diff --git a/app/src/test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java b/app/src/test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java index 683ae047..290d6ff5 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java +++ b/app/src/test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java @@ -25,7 +25,8 @@ public class VaultEntries { new VaultEntry(new HotpInfo(Base32.decode("YOOMIXWS5GN6RTBPUFFWKTW5M4"), "SHA1", 6, 1), "James", "Issuu"), new VaultEntry(new HotpInfo(Base32.decode("KUVJJOM753IHTNDSZVCNKL7GII"), "SHA256", 7, 50), "Benjamin", "Air Canada"), new VaultEntry(new HotpInfo(Base32.decode("5VAML3X35THCEBVRLV24CGBKOY"), "SHA512", 8, 10300), "Mason", "WWE"), - new VaultEntry(new SteamInfo(Base32.decode("JRZCL47CMXVOQMNPZR2F7J4RGI"), "SHA1", 5, 30), "Sophia", "Boeing") + new VaultEntry(new SteamInfo(Base32.decode("JRZCL47CMXVOQMNPZR2F7J4RGI"), "SHA1", 5, 30), "Sophia", "Boeing"), + new VaultEntry(new TotpInfo(Base32.decode("BMGRXPGFARQQF4GMT25JATL2VYLAHDBI"), "SHA1", 8, 30), "US-2211-2050-3346", "Battle.net") ); } catch (OtpInfoException | EncodingException e) { throw new RuntimeException(e); diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/battle_net_authenticator.xml b/app/src/test/resources/com/beemdevelopment/aegis/importers/battle_net_authenticator.xml new file mode 100644 index 00000000..16714447 --- /dev/null +++ b/app/src/test/resources/com/beemdevelopment/aegis/importers/battle_net_authenticator.xml @@ -0,0 +1,7 @@ + + + + 09ec179861450806035080d113c5f05e62f67316110eec1bd495a9cdb65a3cb3f93b1f80b80b4507f0c8894e25fb5d494b31692d76b8bc5fac + + + \ No newline at end of file