From 8d4a68781702d265a4189b1df4228e95bafe51cf Mon Sep 17 00:00:00 2001 From: CristianAUnisa Date: Mon, 11 Apr 2022 21:09:08 +0200 Subject: [PATCH] Add Bitwarden importer Added license notice for Simple Flat Mapper Added Steam secret support to Bitwarden importer https://bitwarden.com/help/authenticator-keys/#steam-guard-totps Added tests --- app/build.gradle | 1 + .../aegis/importers/BitwardenImporter.java | 117 ++++++++++++++++++ .../aegis/importers/DatabaseImporter.java | 1 + app/src/main/res/raw/notices.xml | 6 + app/src/main/res/values/strings.xml | 1 + .../aegis/importers/DatabaseImporterTest.java | 26 ++++ .../aegis/importers/bitwarden.csv | 5 + .../aegis/importers/bitwarden.json | 70 +++++++++++ 8 files changed, 227 insertions(+) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/BitwardenImporter.java create mode 100644 app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.csv create mode 100644 app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.json diff --git a/app/build.gradle b/app/build.gradle index ce578bb2..da0f1794 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -172,6 +172,7 @@ dependencies { implementation 'net.lingala.zip4j:zip4j:2.10.0' implementation 'info.guardianproject.trustedintents:trustedintents:0.2' implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation "org.simpleflatmapper:sfm-csv:8.2.3" androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion" androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion" diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/BitwardenImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/BitwardenImporter.java new file mode 100644 index 00000000..30916be5 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/BitwardenImporter.java @@ -0,0 +1,117 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; +import android.net.Uri; + +import com.beemdevelopment.aegis.encoding.Base32; +import com.beemdevelopment.aegis.encoding.EncodingException; +import com.beemdevelopment.aegis.otp.GoogleAuthInfo; +import com.beemdevelopment.aegis.otp.GoogleAuthInfoException; +import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.otp.SteamInfo; +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 org.simpleflatmapper.csv.CsvParser; +import org.simpleflatmapper.lightningcsv.Row; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class BitwardenImporter extends DatabaseImporter { + public BitwardenImporter(Context context) { + super(context); + } + + @Override + protected SuFile getAppPath() { + throw new UnsupportedOperationException(); + } + + @Override + protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException { + String fileString; + try { + fileString = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new DatabaseImporterException(e); + } + try { + JSONObject obj = new JSONObject(fileString); + JSONArray array = obj.getJSONArray("items"); + + List entries = new ArrayList<>(); + String entry; + for (int i = 0; i < array.length(); i++) { + entry = array.getJSONObject(i).getJSONObject("login").getString("totp"); + if (!entry.isEmpty()) { + entries.add(entry); + } + } + + return new BitwardenImporter.State(entries); + } catch (JSONException e) { + try { + Iterator rowIterator = CsvParser.separator(',').rowIterator(fileString); + List entries = new ArrayList<>(); + rowIterator.forEachRemaining((row -> { + String entry = row.get("login_totp"); + if (entry != null && !entry.isEmpty()) { + entries.add(entry); + } + })); + return new BitwardenImporter.State(entries); + } catch (IOException e2) { + throw new DatabaseImporterException(e2); + } + } + } + + 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 obj : _entries) { + try { + VaultEntry entry = convertEntry(obj); + result.addEntry(entry); + } catch (DatabaseImporterEntryException e) { + result.addError(e); + } + } + + return result; + } + + private static VaultEntry convertEntry(String obj) throws DatabaseImporterEntryException { + try { + GoogleAuthInfo info = BitwardenImporter.parseUri(obj); + return new VaultEntry(info); + } catch (GoogleAuthInfoException | EncodingException | OtpInfoException | URISyntaxException e) { + throw new DatabaseImporterEntryException(e, obj); + } + } + } + + private static GoogleAuthInfo parseUri(String obj) throws EncodingException, OtpInfoException, URISyntaxException, GoogleAuthInfoException { + Uri uri = Uri.parse(obj); + return uri.getScheme().equals("steam") ? new GoogleAuthInfo(new SteamInfo(Base32.decode(uri.getAuthority())), "Steam account", "Steam") : GoogleAuthInfo.parseUri(uri); + } +} 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 d64d211f..7f2638e3 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("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)); _importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true)); diff --git a/app/src/main/res/raw/notices.xml b/app/src/main/res/raw/notices.xml index 2bc2f340..8d4d2a63 100644 --- a/app/src/main/res/raw/notices.xml +++ b/app/src/main/res/raw/notices.xml @@ -66,6 +66,12 @@ https://github.com/protocolbuffers/protobuf/tree/master/java Protocol Buffers License + + Simple Flat Mapper + https://github.com/arnaudroger/SimpleFlatMapper + Copyright (c) 2014 Arnaud Roger + MIT License + TextDrawable https://github.com/amulyakhare/TextDrawable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f29a48a9..38a79365 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,6 +377,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 Bitwarden export/backup file. Encrypted files are not supported. 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 5ba589e5..2ea5576d 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.importers; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -168,6 +169,18 @@ public class DatabaseImporterTest { checkImportedAuthyEntries(entries); } + @Test + public void testImportBitwardenJson() throws IOException, DatabaseImporterException { + List entries = importPlain(BitwardenImporter.class, "bitwarden.json"); + checkImportedBitwardenEntries(entries); + } + + @Test + public void testImportBitwardenCsv() throws IOException, DatabaseImporterException { + List entries = importPlain(BitwardenImporter.class, "bitwarden.csv"); + checkImportedBitwardenEntries(entries); + } + @Test public void testImportFreeOtp() throws IOException, DatabaseImporterException, OtpInfoException { List entries = importPlain(FreeOtpImporter.class, "freeotp.xml"); @@ -347,6 +360,19 @@ public class DatabaseImporterTest { } } + private void checkImportedBitwardenEntries(List entries) { + byte[] secret, vectorSecret; + for (VaultEntry entry : entries) { + if(entry.getInfo().getTypeId().equals(SteamInfo.ID)) { + secret = entry.getInfo().getSecret(); + vectorSecret = getEntryVectorBySecret(secret).getInfo().getSecret(); + assertNotNull(String.format("Steam secret has not been found (%s)", vectorSecret)); + } else { + checkImportedEntry(entry); + } + } + } + private void checkImportedEntries(List entries) { for (VaultEntry entry : entries) { checkImportedEntry(entry); diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.csv b/app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.csv new file mode 100644 index 00000000..89edce6c --- /dev/null +++ b/app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.csv @@ -0,0 +1,5 @@ +folder,favorite,type,name,notes,fields,login_uri,login_username,login_password,login_totp +,,login,Test 1,,,,,,otpauth://totp/Deno:Mason?secret=4SJHB4GSD43FZBAI7C2HLRJGPQ&issuer=Deno&algorithm=SHA1&digits=6&period=30 +,,login,Test 2,,,,,,otpauth://totp/SPDX:James?secret=5OM4WOOGPLQEF6UGN3CPEOOLWU&issuer=SPDX&algorithm=SHA256&digits=7&period=20 +,,login,Test 3,,,,,,otpauth://totp/Airbnb:Elijah?secret=7ELGJSGXNCCTV3O6LKJWYFV2RA&issuer=Airbnb&algorithm=SHA512&digits=8&period=50 +,,login,Test 4,,,,,,steam://JRZCL47CMXVOQMNPZR2F7J4RGI \ No newline at end of file diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.json b/app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.json new file mode 100644 index 00000000..81c57006 --- /dev/null +++ b/app/src/test/resources/com/beemdevelopment/aegis/importers/bitwarden.json @@ -0,0 +1,70 @@ +{ + "encrypted": false, + "folders": [], + "items": [ + { + "id": "ffdea2cc-923d-47a0-be99-ae7800fd51e4", + "organizationId": null, + "folderId": null, + "type": 1, + "name": "Test 1", + "notes": null, + "favorite": false, + "login": { + "uris": null, + "username": null, + "password": null, + "totp": "otpauth://totp/Deno:Mason?secret=4SJHB4GSD43FZBAI7C2HLRJGPQ&issuer=Deno&algorithm=SHA1&digits=6&period=30" + }, + "collectionIds": null + }, + { + "id": "0f954b25-dbe9-4b38-b8ff-ae7800fd784d", + "organizationId": null, + "folderId": null, + "type": 1, + "name": "Test 2", + "notes": null, + "favorite": false, + "login": { + "uris": null, + "username": null, + "password": null, + "totp": "otpauth://totp/SPDX:James?secret=5OM4WOOGPLQEF6UGN3CPEOOLWU&issuer=SPDX&algorithm=SHA256&digits=7&period=20" + }, + "collectionIds": null + }, + { + "id": "b0f3aae6-f4da-49bc-b981-ae7800fd9467", + "organizationId": null, + "folderId": null, + "type": 1, + "name": "Test 3", + "notes": null, + "favorite": false, + "login": { + "uris": null, + "username": null, + "password": null, + "totp": "otpauth://totp/Airbnb:Elijah?secret=7ELGJSGXNCCTV3O6LKJWYFV2RA&issuer=Airbnb&algorithm=SHA512&digits=8&period=50" + }, + "collectionIds": null + }, + { + "id": "eb10632a-3bfd-40cd-9e4c-8c01c020b944", + "organizationId": null, + "folderId": null, + "type": 1, + "name": "Test Steam", + "notes": null, + "favorite": false, + "login": { + "uris": null, + "username": null, + "password": null, + "totp": "steam://JRZCL47CMXVOQMNPZR2F7J4RGI" + }, + "collectionIds": null + } + ] +} \ No newline at end of file