Merge pull request #1039 from CristianAUnisa/export-to-html

Export vault to HTML
This commit is contained in:
Alexander Bakker 2022-12-04 23:01:25 +01:00 committed by GitHub
commit bebda569de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 2 deletions

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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("<html><head><title>");
printStream.print(_context.getString(R.string.export_html_title));
printStream.print("</title></head><body>");
printStream.print("<h1>");
printStream.print(_context.getString(R.string.export_html_title));
printStream.print("</h1>");
printStream.print("<table>");
printStream.print("<tr>");
printStream.print("<th>Issuer</th>");
printStream.print("<th>Username</th>");
printStream.print("<th>Type</th>");
printStream.print("<th>QR Code</th>");
printStream.print("<th>UUID</th>");
printStream.print("<th>Note</th>");
printStream.print("<th>Favorite</th>");
printStream.print("<th>Algo</th>");
printStream.print("<th>Digits</th>");
printStream.print("<th>Secret</th>");
printStream.print("<th>Counter</th>");
printStream.print("</tr>");
for (VaultEntry entry : getEntries()) {
if (filter == null || filter.includeEntry(entry)) {
printStream.print("<tr>");
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
OtpInfo otpInfo = info.getOtpInfo();
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(info.getIssuer()));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getName()));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(otpInfo.getType()));
printStream.print("</td>");
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("<td class='qr'><img src=\"data:image/png;base64,");
printStream.print(encodedImage);
printStream.print("\"/></td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getUUID().toString()));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(entry.getNote()));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(entry.isFavorite() ? "true" : "false"));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(otpInfo.getAlgorithm(false)));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(Integer.toString(otpInfo.getDigits())));
printStream.print("</td>");
printStream.print("<td>");
printStream.print(HtmlEscapers.htmlEscaper().escape(Base32.encode(otpInfo.getSecret())));
printStream.print("</td>");
printStream.print("<td>");
if (Objects.equals(otpInfo.getTypeId(), HotpInfo.ID)) {
printStream.print(HtmlEscapers.htmlEscaper().escape(Long.toString(((HotpInfo) otpInfo).getCounter())));
} else {
printStream.print("-");
}
printStream.print("</td>");
printStream.print("</tr>");
}
};
printStream.print("</table></body>");
printStream.print("<style>table,td,th{border:1px solid #000;border-collapse:collapse;text-align:center}td:not(.qr),th{padding:1em}</style>");
printStream.print("</html>");
printStream.flush();
} catch (WriterException | IOException e) {
throw new VaultRepositoryException(e);
}
}
public void addEntry(VaultEntry entry) {
_vault.getEntries().add(entry);
}

View File

@ -128,6 +128,7 @@
<string-array name="export_formats">
<item>@string/export_format_aegis</item>
<item>@string/export_format_html</item>
<item>@string/export_format_google_auth_uri</item>
</string-array>

View File

@ -105,10 +105,12 @@
<string name="export_warning_password">Exports are encrypted using a separate password configured in Security settings.</string>
<string name="export_format_aegis">Aegis (.JSON)</string>
<string name="export_format_google_auth_uri">Text file (.TXT)</string>
<string name="export_format_html">Web page (.HTML)</string>
<string name="export_format_hint">Export format</string>
<string name="export_all_groups">Export all groups</string>
<string name="export_choose_groups">Select which groups to export:</string>
<string name="export_no_groups_selected">No groups selected to export</string>
<string name="export_html_title">Aegis Authenticator Export</string>
<string name="choose_authentication_method">Security</string>
<string name="authentication_method_explanation">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.</string>