diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5a6616bf..e375fbfc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -149,6 +149,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 daf8d416..24a2153c 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
@@ -33,6 +33,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));
@@ -102,6 +103,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 extends DatabaseImporter> 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 383bc18e..f1ce9fb9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -441,6 +441,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 @@
+
+
\ No newline at end of file