From 59c887e6a42c994235b4118c5955da923ed9418e Mon Sep 17 00:00:00 2001 From: CristianAUnisa Date: Fri, 25 Nov 2022 23:26:53 +0100 Subject: [PATCH] Export vault to HTML --- .../aegis/BackupExportTest.java | 26 +++++ .../ImportExportPreferencesFragment.java | 17 +++- .../preferences/PreferencesFragment.java | 3 +- .../aegis/vault/VaultRepository.java | 99 +++++++++++++++++++ app/src/main/res/values/arrays.xml | 1 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 146 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/BackupExportTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/BackupExportTest.java index 36615f44..9f15019b 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/BackupExportTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/BackupExportTest.java @@ -173,6 +173,32 @@ public class BackupExportTest extends AegisTest { readVault(file, VAULT_PASSWORD); } + @Test + public void testPlainVaultExportHtml() { + initPlainVault(); + + openExportDialog(); + onView(withId(R.id.checkbox_export_encrypt)).perform(click()); + onView(withId(R.id.dropdown_export_format)).perform(click()); + onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click()); + onView(withId(android.R.id.button1)).perform(click()); + onView(withId(R.id.checkbox_accept)).perform(click()); + doExport(); + } + + @Test + public void testEncryptedVaultExportHtml() { + initEncryptedVault(); + + openExportDialog(); + onView(withId(R.id.checkbox_export_encrypt)).perform(click()); + onView(withId(R.id.dropdown_export_format)).perform(click()); + onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click()); + onView(withId(android.R.id.button1)).perform(click()); + onView(withId(R.id.checkbox_accept)).perform(click()); + doExport(); + } + @Test public void testSeparateExportPassword() { initEncryptedVault(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java index 9f5d2f04..6c5478a6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java @@ -122,6 +122,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { // intentional fallthrough case CODE_EXPORT_PLAIN: // intentional fallthrough + case CODE_EXPORT_HTML: + // intentional fallthrough case CODE_EXPORT_GOOGLE_URI: onExportResult(requestCode, resultCode, data); break; @@ -361,6 +363,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { private static int getExportRequestCode(int spinnerPos, boolean encrypt) { if (spinnerPos == 0) { return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN; + } else if (spinnerPos == 1) { + return CODE_EXPORT_HTML; } return CODE_EXPORT_GOOGLE_URI; @@ -370,13 +374,20 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { if (spinnerPos == 0) { String filename = encrypt ? VaultRepository.FILENAME_PREFIX_EXPORT : VaultRepository.FILENAME_PREFIX_EXPORT_PLAIN; return new VaultBackupManager.FileInfo(filename); + } else if (spinnerPos == 1) { + return new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT_HTML, "html"); } return new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT_URI, "txt"); } private static String getExportMimeType(int requestCode) { - return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json"; + if (requestCode == CODE_EXPORT_GOOGLE_URI) { + return "text/plain"; + } else if (requestCode == CODE_EXPORT_HTML) { + return "text/html"; + } + return "application/json"; } private File getExportCacheDir() throws IOException { @@ -444,6 +455,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { cb.exportVault((stream) -> _vaultManager.getVault().exportGoogleUris(stream, filter)); _prefs.setIsPlaintextBackupWarningNeeded(true); break; + case CODE_EXPORT_HTML: + cb.exportVault((stream) -> _vaultManager.getVault().exportHtml(stream, filter)); + _prefs.setIsPlaintextBackupWarningNeeded(true); + break; } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java index 8958cbcf..88e53ff7 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java @@ -28,7 +28,8 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat { public static final int CODE_EXPORT = 5; public static final int CODE_EXPORT_PLAIN = 6; public static final int CODE_EXPORT_GOOGLE_URI = 7; - public static final int CODE_BACKUPS = 8; + public static final int CODE_EXPORT_HTML = 8; + public static final int CODE_BACKUPS = 9; private Intent _result; diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java index 4fff6440..3aa5af4f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -1,17 +1,28 @@ package com.beemdevelopment.aegis.vault; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.AtomicFile; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.encoding.Base32; +import com.beemdevelopment.aegis.helpers.QrCodeHelper; import com.beemdevelopment.aegis.otp.GoogleAuthInfo; +import com.beemdevelopment.aegis.otp.HotpInfo; +import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.util.IOUtils; +import com.google.common.html.HtmlEscapers; +import com.google.zxing.WriterException; import org.json.JSONObject; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -21,6 +32,7 @@ import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.text.Collator; import java.util.Collection; +import java.util.Objects; import java.util.TreeSet; import java.util.UUID; @@ -29,6 +41,7 @@ public class VaultRepository { public static final String FILENAME_PREFIX_EXPORT = "aegis-export"; public static final String FILENAME_PREFIX_EXPORT_PLAIN = "aegis-export-plain"; public static final String FILENAME_PREFIX_EXPORT_URI = "aegis-export-uri"; + public static final String FILENAME_PREFIX_EXPORT_HTML = "aegis-export-html"; @NonNull private final Vault _vault; @@ -197,6 +210,92 @@ public class VaultRepository { } } + /** + * Exports the vault by serializing the list of entries to an HTML file containing the Issuer, + * Username and QR Code and writing it to the given OutputStream. + */ + public void exportHtml(OutputStream outStream, @Nullable Vault.EntryFilter filter) throws VaultRepositoryException { + try { + PrintStream printStream = new PrintStream(outStream, false, StandardCharsets.UTF_8.name()); + printStream.print(""); + printStream.print(_context.getString(R.string.export_html_title)); + printStream.print(""); + printStream.print("

"); + printStream.print(_context.getString(R.string.export_html_title)); + printStream.print("

"); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + for (VaultEntry entry : getEntries()) { + if (filter == null || filter.includeEntry(entry)) { + printStream.print(""); + GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer()); + OtpInfo otpInfo = info.getOtpInfo(); + printStream.print(""); + printStream.print(""); + printStream.print(""); + Bitmap bm = QrCodeHelper.encodeToBitmap(info.getUri().toString(),256, 256, Color.WHITE); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bm.compress(Bitmap.CompressFormat.PNG, 100, baos); + byte[] b = baos.toByteArray(); + String encodedImage = Base64.encodeToString(b, Base64.DEFAULT); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + printStream.print(""); + } + }; + printStream.print("
IssuerUsernameTypeQR CodeUUIDNoteFavoriteAlgoDigitsSecretCounter
"); + printStream.print(HtmlEscapers.htmlEscaper().escape(info.getIssuer())); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getName())); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(otpInfo.getType())); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getUUID().toString())); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getNote())); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.isFavorite() ? "true" : "false")); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(otpInfo.getAlgorithm(false))); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(Integer.toString(otpInfo.getDigits()))); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(Base32.encode(otpInfo.getSecret()))); + printStream.print(""); + if (Objects.equals(otpInfo.getTypeId(), HotpInfo.ID)) { + printStream.print(HtmlEscapers.htmlEscaper().escape(Long.toString(((HotpInfo) otpInfo).getCounter()))); + } else { + printStream.print("-"); + } + printStream.print("
"); + printStream.print(""); + printStream.print(""); + printStream.flush(); + } catch (WriterException | IOException e) { + throw new VaultRepositoryException(e); + } + } + public void addEntry(VaultEntry entry) { _vault.getEntries().add(entry); } diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index ee4cea43..11f4f162 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -128,6 +128,7 @@ @string/export_format_aegis + @string/export_format_html @string/export_format_google_auth_uri diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 383bc18e..7f1fc226 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,10 +105,12 @@ Exports are encrypted using a separate password configured in Security settings. Aegis (.JSON) Text file (.TXT) + Web page (.HTML) Export format Export all groups Select which groups to export: No groups selected to export + Aegis Authenticator Export Security Aegis is a security-focused 2FA app. Tokens are stored in a vault, that can optionally be encrypted with a password of your choosing. If an attacker obtains your encrypted vault file, they will not be able to access the contents without knowing the password.\n\nWe\'ve preselected the option that we think would fit best for your device.