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 b2bf64f5..f22a6f90 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("
Issuer | "); + printStream.print("Username | "); + printStream.print("Type | "); + printStream.print("QR Code | "); + printStream.print("UUID | "); + printStream.print("Note | "); + printStream.print("Favorite | "); + printStream.print("Algo | "); + printStream.print("Digits | "); + printStream.print("Secret | "); + printStream.print("Counter | "); + printStream.print("
---|---|---|---|---|---|---|---|---|---|---|
"); + printStream.print(HtmlEscapers.htmlEscaper().escape(info.getIssuer())); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getName())); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(otpInfo.getType())); + 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(HtmlEscapers.htmlEscaper().escape(entry.getUUID().toString())); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getNote())); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(entry.isFavorite() ? "true" : "false")); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(otpInfo.getAlgorithm(false))); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(Integer.toString(otpInfo.getDigits()))); + printStream.print(" | "); + printStream.print(""); + printStream.print(HtmlEscapers.htmlEscaper().escape(Base32.encode(otpInfo.getSecret()))); + printStream.print(" | "); + 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("