Import from ZIP for the Authenticator Plus, instead of SQLite databases

This commit is contained in:
Alexander Bakker 2020-05-10 17:49:03 +02:00
parent e41f200bcd
commit 1824d38f75
4 changed files with 26 additions and 156 deletions

View file

@ -94,6 +94,7 @@ dependencies {
def libsuVersion = '2.5.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
implementation 'net.lingala.zip4j:zip4j:2.5.2'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.biometric:biometric:1.0.1"
@ -118,7 +119,6 @@ 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'

View file

@ -17,6 +17,5 @@
#}
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class net.sqlcipher.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

View file

@ -1,24 +1,19 @@
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 net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
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";
private static final String FILENAME = "Accounts.txt";
public AuthenticatorPlusImporter(Context context) {
super(context);
@ -26,7 +21,7 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
@Override
protected String getAppPkgName() {
return _pkgName;
throw new UnsupportedOperationException();
}
@Override
@ -54,123 +49,25 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try {
// recreate InputStream from saved byte array
InputStream stream = new ByteArrayInputStream(_data);
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
FileReader reader = new FileReader(zipStream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
GoogleAuthUriImporter.State state = importer.read(reader);
listener.onStateDecrypted(state);
return;
}
}
SqlImporterHelper helper = new SqlImporterHelper(context, password, true);
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
DecryptedState state = new DecryptedState(entries);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
throw new FileNotFoundException(FILENAME);
} catch (IOException | DatabaseImporterException e) {
listener.onError(e);
}
});
}
}
public static class DecryptedState extends DatabaseImporter.State {
private List<Entry> _entries;
private DecryptedState(List<Entry> 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];
}
// Authenticator Plus saves all entries to the "All Accounts" category by default
// If an entry is in this category, we can consider it as uncategorized
String group = entry.getGroup();
if (group.equals("All Accounts")) {
return new VaultEntry(info, name, entry.getIssuer());
}
return new VaultEntry(info, name, entry.getIssuer(), entry.getGroup());
} 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 String _group;
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", "");
_group = SqlImporterHelper.getString(cursor, "category", "");
_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 String getGroup() {
return _group;
}
public long getCounter() {
return _counter;
}
}
}

View file

@ -2,13 +2,11 @@ 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;
@ -21,18 +19,9 @@ 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 <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
@ -48,22 +37,7 @@ public class SqlImporterHelper {
throw new DatabaseImporterException(e);
}
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 (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
List<T> entries = new ArrayList<>();