diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8132f216..729ce17f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -116,6 +116,7 @@
+
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 2e5fa4b4..cd72dca4 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
@@ -32,6 +32,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("DUO", DuoImporter.class, R.string.importer_help_duo, true));
_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));
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DuoImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DuoImporter.java
new file mode 100644
index 00000000..b449e940
--- /dev/null
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DuoImporter.java
@@ -0,0 +1,103 @@
+package com.beemdevelopment.aegis.importers;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.beemdevelopment.aegis.encoding.Base32;
+import com.beemdevelopment.aegis.encoding.EncodingException;
+import com.beemdevelopment.aegis.otp.HotpInfo;
+import com.beemdevelopment.aegis.otp.OtpInfo;
+import com.beemdevelopment.aegis.otp.OtpInfoException;
+import com.beemdevelopment.aegis.otp.TotpInfo;
+import com.beemdevelopment.aegis.util.IOUtils;
+import com.beemdevelopment.aegis.vault.VaultEntry;
+import com.topjohnwu.superuser.io.SuFile;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class DuoImporter extends DatabaseImporter {
+ private static final String _pkgName = "com.duosecurity.duomobile";
+ private static final String _subPath = "files/duokit/accounts.json";
+
+ public DuoImporter(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected @NonNull SuFile getAppPath() throws DatabaseImporterException, NameNotFoundException {
+ return getAppPath(_pkgName, _subPath);
+ }
+
+ @Override
+ protected @NonNull State read(
+ @NonNull InputStream stream, boolean isInternal
+ ) throws DatabaseImporterException {
+ try {
+ String contents = new String(IOUtils.readAll(stream), UTF_8);
+ return new DecryptedState(new JSONArray(contents));
+ } catch (JSONException | IOException e) {
+ throw new DatabaseImporterException(e);
+ }
+ }
+
+ public static class DecryptedState extends DatabaseImporter.State {
+ private final JSONArray _array;
+
+ public DecryptedState(@NonNull JSONArray array) {
+ super(false);
+ _array = array;
+ }
+
+ @Override
+ public @NonNull Result convert() throws DatabaseImporterException {
+ Result result = new Result();
+
+ try {
+ for (int i = 0; i < _array.length(); i++) {
+ JSONObject entry = _array.getJSONObject(i);
+ try {
+ result.addEntry(convertEntry(entry));
+ } catch (DatabaseImporterEntryException e) {
+ result.addError(e);
+ }
+ }
+ } catch (JSONException e) {
+ throw new DatabaseImporterException(e);
+ }
+
+ return result;
+ }
+
+ private static @NonNull VaultEntry convertEntry(
+ @NonNull JSONObject entry
+ ) throws DatabaseImporterEntryException {
+ try {
+ String label = entry.optString("name");
+
+ JSONObject otpData = entry.getJSONObject("otpGenerator");
+
+ byte[] secret = Base32.decode(otpData.getString("otpSecret"));
+
+ Long counter = otpData.has("counter") ? otpData.getLong("counter") : null;
+
+ OtpInfo otp = counter == null
+ ? new TotpInfo(secret)
+ : new HotpInfo(secret, counter);
+
+ return new VaultEntry(otp, label, "");
+ } catch (JSONException | OtpInfoException | EncodingException e) {
+ throw new DatabaseImporterEntryException(e, entry.toString());
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml
index cadf7b53..d5fe5aaf 100644
--- a/app/src/main/res/values-ru-rRU/strings.xml
+++ b/app/src/main/res/values-ru-rRU/strings.xml
@@ -370,6 +370,7 @@
Предоставьте файл экспорта Authenticator Plus, полученный через «Настройки» → «Рез. копия и восстановление» → «Экспорт текста и HTML».
Предоставьте копию файла /data/data/com.authy.authy/shared_prefs/com.authy.storage.tokens.authenticator.xml, расположенного в папке Authy во внутренней памяти.
Предоставьте файл экспорта/резервной копии andOTP.
+ Предоставьте копию файла /data/data/com.duosecurity.duomobile/files/duokit/accounts.json, расположенного в папке DUO во внутренней памяти.
Предоставьте копию файла /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml, расположенного в папке FreeOTP во внутренней памяти.
Предоставьте файл экспорта FreeOTP+.
Предоставьте копию файла /data/data/com.google.android.apps.authenticator2/databases/databases, расположенного в папке Google Authenticator во внутренней памяти.
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index cc4c4d86..2d7b675e 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -337,6 +337,7 @@
Надайте файл експорту Authenticator Plus, отриманий через Settings -> Backup & Restore -> Export as Text and HTML.
Надайте копію /data/com.authy.authy/shared_prefs/com.authy.storage.tokens.authenticator.xml, що знаходиться в каталозі Authy у внутрішній пам\'яті.
Надайте файл експорту/резервної копії andOTP.
+ Надайте копію /data/data/com.duosecurity.duomobile/files/duokit/accounts.json, що знаходиться в каталозі DUO у внутрішній пам\'яті.
Надайте копію /data/org.fedorahosted.freeotp/shared_prefs/tokens.xml, що знаходиться в каталозі FreeOTP у внутрішній пам\'яті.
Надайте файл експортований з FreeOTP+.
Надайте копію /data/com.google.android.apps.authenticator2/databases/databases, що знаходиться в каталозі Google Authenticator у внутрішній пам\'яті.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bcf88bfb..e50b47c9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -391,6 +391,7 @@
Supply an Authenticator Plus export file obtained through Settings -> Backup & Restore -> Export as Text and HTML.
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 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.
Supply a copy of /data/data/com.google.android.apps.authenticator2/databases/databases, located in the internal storage directory of Google Authenticator.