From dc0f7c466835128b8d453aa4011117bce1c8e516 Mon Sep 17 00:00:00 2001 From: orangenbaumblatt Date: Thu, 12 Mar 2020 18:34:17 +0100 Subject: [PATCH] Added Database Import from Authenticator Plus Added copyright notice Rearrange item position in import dialog Refactor SqlImportHelper to support SQLCipher Improved stream handling --- app/build.gradle | 1 + .../importers/AuthenticatorPlusImporter.java | 163 ++++++++++++++++++ .../aegis/importers/DatabaseImporter.java | 1 + .../aegis/importers/SqlImporterHelper.java | 34 +++- app/src/main/res/raw/notices.xml | 5 + 5 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorPlusImporter.java diff --git a/app/build.gradle b/app/build.gradle index 0be15093..652c77e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,6 +90,7 @@ dependencies { implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") { transitive = false } + implementation 'net.zetetic:android-database-sqlcipher:4.3.0' annotationProcessor 'androidx.annotation:annotation:1.1.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2' diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorPlusImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorPlusImporter.java new file mode 100644 index 00000000..5a13fa16 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorPlusImporter.java @@ -0,0 +1,163 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; +import android.database.Cursor; + +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.ui.Dialogs; +import com.beemdevelopment.aegis.vault.VaultEntry; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class AuthenticatorPlusImporter extends DatabaseImporter { + private static final String _pkgName = "com.mufri.authenticatorplus"; + + public AuthenticatorPlusImporter(Context context) { + super(context); + } + + @Override + protected String getAppPkgName() { + return _pkgName; + } + + @Override + protected String getAppSubPath() { + throw new UnsupportedOperationException(); + } + + @Override + public State read(FileReader reader) throws DatabaseImporterException { + try { + return new EncryptedState(reader.readAll()); + } catch (IOException e) { + throw new DatabaseImporterException(e); + } + } + + public static class EncryptedState extends DatabaseImporter.State { + private byte[] _data; + + private EncryptedState(byte[] data) { + super(true); + _data = data; + } + + @Override + public void decrypt(Context context, DecryptListener listener) { + Dialogs.showPasswordInputDialog(context, password -> { + try { + // recreate InputStream from saved byte array + InputStream stream = new ByteArrayInputStream(_data); + + SqlImporterHelper helper = new SqlImporterHelper(context, password, true); + List entries = helper.read(Entry.class, stream, "accounts"); + + DecryptedState state = new DecryptedState(entries); + listener.onStateDecrypted(state); + } catch (DatabaseImporterException e) { + listener.onError(e); + } + }); + } + } + + public static class DecryptedState extends DatabaseImporter.State { + private List _entries; + + private DecryptedState(List entries) { + super(false); + _entries = entries; + } + + private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException { + try { + String secretString = entry.getSecret().replaceAll("\\s", ""); + byte[] secret = Base32.decode(secretString); + + OtpInfo info; + switch (entry.getType()) { + case 0: + info = new TotpInfo(secret); + break; + case 1: + info = new HotpInfo(secret, entry.getCounter()); + break; + default: + throw new DatabaseImporterException("unsupported otp type: " + entry.getType()); + } + + String name = entry.getEmail(); + String[] parts = name.split(":"); + if (parts.length == 2) { + name = parts[1]; + } + + return new VaultEntry(info, name, entry.getIssuer()); + } catch (EncodingException | OtpInfoException | DatabaseImporterException e) { + throw new DatabaseImporterEntryException(e, entry.toString()); + } + } + + @Override + public Result convert() { + Result result = new Result(); + + for (Entry sqlEntry : _entries) { + try { + VaultEntry entry = convertEntry(sqlEntry); + result.addEntry(entry); + } catch (DatabaseImporterEntryException e) { + result.addError(e); + } + } + + return result; + } + } + + private static class Entry extends SqlImporterHelper.Entry { + private int _type; + private String _secret; + private String _email; + private String _issuer; + private long _counter; + + public Entry(Cursor cursor) { + super(cursor); + _type = SqlImporterHelper.getInt(cursor, "type"); + _secret = SqlImporterHelper.getString(cursor, "secret"); + _email = SqlImporterHelper.getString(cursor, "email", ""); + _issuer = SqlImporterHelper.getString(cursor, "issuer", ""); + _counter = SqlImporterHelper.getLong(cursor, "counter"); + } + + public int getType() { + return _type; + } + + public String getSecret() { + return _secret; + } + + public String getEmail() { + return _email; + } + + public String getIssuer() { + return _issuer; + } + + public long getCounter() { + return _counter; + } + } +} 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 494422c2..dffd4619 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -27,6 +27,7 @@ public abstract class DatabaseImporter { // note: keep these lists sorted alphabetically _importers = new LinkedHashMap<>(); _importers.put("Aegis", AegisImporter.class); + _importers.put("Authenticator Plus", AuthenticatorPlusImporter.class); _importers.put("Authy", AuthyImporter.class); _importers.put("andOTP", AndOtpImporter.class); _importers.put("FreeOTP", FreeOtpImporter.class); diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java b/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java index 15abc0c1..8e395626 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java @@ -2,11 +2,13 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; import com.topjohnwu.superuser.ShellUtils; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SQLiteException; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -19,9 +21,18 @@ import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY; public class SqlImporterHelper { private Context _context; + private char[] _password; + private boolean _compatibilityMode; public SqlImporterHelper(Context context) { _context = context; + _password = null; + } + + public SqlImporterHelper(Context context, char[] password, boolean compatibilityMode) { + _context = context; + _password = password; + _compatibilityMode = compatibilityMode; } public List read(Class type, InputStream inStream, String table) throws DatabaseImporterException { @@ -37,7 +48,22 @@ public class SqlImporterHelper { throw new DatabaseImporterException(e); } - try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) { + SQLiteDatabase.loadLibs(_context); + SQLiteDatabaseHook compatibilityHook = new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase database) { + + } + + @Override + public void postKey(SQLiteDatabase database) { + if (_compatibilityMode) { + database.compileStatement("PRAGMA cipher_compatibility = 3;"); + } + } + }; + + try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), _password, null, OPEN_READONLY, compatibilityHook)) { try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) { List entries = new ArrayList<>(); @@ -82,7 +108,7 @@ public class SqlImporterHelper { } public static abstract class Entry { - public Entry (Cursor cursor) { + public Entry(Cursor cursor) { } } diff --git a/app/src/main/res/raw/notices.xml b/app/src/main/res/raw/notices.xml index c46052ac..2ea9edaa 100644 --- a/app/src/main/res/raw/notices.xml +++ b/app/src/main/res/raw/notices.xml @@ -70,4 +70,9 @@ Copyright (C) 2016 The Android Open Source Project Apache Software License 2.0 + + SQLCipher for Android + https://github.com/sqlcipher/android-database-sqlcipher + Apache Software License 2.0 + \ No newline at end of file