mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-16 23:12:51 +00:00
Export vault to HTML
This commit is contained in:
parent
ef759eb15e
commit
59c887e6a4
6 changed files with 146 additions and 2 deletions
|
@ -173,6 +173,32 @@ public class BackupExportTest extends AegisTest {
|
||||||
readVault(file, VAULT_PASSWORD);
|
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
|
@Test
|
||||||
public void testSeparateExportPassword() {
|
public void testSeparateExportPassword() {
|
||||||
initEncryptedVault();
|
initEncryptedVault();
|
||||||
|
|
|
@ -122,6 +122,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
// intentional fallthrough
|
// intentional fallthrough
|
||||||
case CODE_EXPORT_PLAIN:
|
case CODE_EXPORT_PLAIN:
|
||||||
// intentional fallthrough
|
// intentional fallthrough
|
||||||
|
case CODE_EXPORT_HTML:
|
||||||
|
// intentional fallthrough
|
||||||
case CODE_EXPORT_GOOGLE_URI:
|
case CODE_EXPORT_GOOGLE_URI:
|
||||||
onExportResult(requestCode, resultCode, data);
|
onExportResult(requestCode, resultCode, data);
|
||||||
break;
|
break;
|
||||||
|
@ -361,6 +363,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
|
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
|
||||||
if (spinnerPos == 0) {
|
if (spinnerPos == 0) {
|
||||||
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
|
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
|
||||||
|
} else if (spinnerPos == 1) {
|
||||||
|
return CODE_EXPORT_HTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CODE_EXPORT_GOOGLE_URI;
|
return CODE_EXPORT_GOOGLE_URI;
|
||||||
|
@ -370,13 +374,20 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
if (spinnerPos == 0) {
|
if (spinnerPos == 0) {
|
||||||
String filename = encrypt ? VaultRepository.FILENAME_PREFIX_EXPORT : VaultRepository.FILENAME_PREFIX_EXPORT_PLAIN;
|
String filename = encrypt ? VaultRepository.FILENAME_PREFIX_EXPORT : VaultRepository.FILENAME_PREFIX_EXPORT_PLAIN;
|
||||||
return new VaultBackupManager.FileInfo(filename);
|
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");
|
return new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT_URI, "txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getExportMimeType(int requestCode) {
|
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 {
|
private File getExportCacheDir() throws IOException {
|
||||||
|
@ -444,6 +455,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
cb.exportVault((stream) -> _vaultManager.getVault().exportGoogleUris(stream, filter));
|
cb.exportVault((stream) -> _vaultManager.getVault().exportGoogleUris(stream, filter));
|
||||||
_prefs.setIsPlaintextBackupWarningNeeded(true);
|
_prefs.setIsPlaintextBackupWarningNeeded(true);
|
||||||
break;
|
break;
|
||||||
|
case CODE_EXPORT_HTML:
|
||||||
|
cb.exportVault((stream) -> _vaultManager.getVault().exportHtml(stream, filter));
|
||||||
|
_prefs.setIsPlaintextBackupWarningNeeded(true);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||||
public static final int CODE_EXPORT = 5;
|
public static final int CODE_EXPORT = 5;
|
||||||
public static final int CODE_EXPORT_PLAIN = 6;
|
public static final int CODE_EXPORT_PLAIN = 6;
|
||||||
public static final int CODE_EXPORT_GOOGLE_URI = 7;
|
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;
|
private Intent _result;
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
package com.beemdevelopment.aegis.vault;
|
package com.beemdevelopment.aegis.vault;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.util.AtomicFile;
|
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.GoogleAuthInfo;
|
||||||
|
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||||
|
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||||
import com.beemdevelopment.aegis.util.IOUtils;
|
import com.beemdevelopment.aegis.util.IOUtils;
|
||||||
|
import com.google.common.html.HtmlEscapers;
|
||||||
|
import com.google.zxing.WriterException;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -21,6 +32,7 @@ import java.io.PrintStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.Collator;
|
import java.text.Collator;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.UUID;
|
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 = "aegis-export";
|
||||||
public static final String FILENAME_PREFIX_EXPORT_PLAIN = "aegis-export-plain";
|
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_URI = "aegis-export-uri";
|
||||||
|
public static final String FILENAME_PREFIX_EXPORT_HTML = "aegis-export-html";
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Vault _vault;
|
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) {
|
public void addEntry(VaultEntry entry) {
|
||||||
_vault.getEntries().add(entry);
|
_vault.getEntries().add(entry);
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,7 @@
|
||||||
|
|
||||||
<string-array name="export_formats">
|
<string-array name="export_formats">
|
||||||
<item>@string/export_format_aegis</item>
|
<item>@string/export_format_aegis</item>
|
||||||
|
<item>@string/export_format_html</item>
|
||||||
<item>@string/export_format_google_auth_uri</item>
|
<item>@string/export_format_google_auth_uri</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
|
|
@ -105,10 +105,12 @@
|
||||||
<string name="export_warning_password">Exports are encrypted using a separate password configured in Security settings.</string>
|
<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_aegis">Aegis (.JSON)</string>
|
||||||
<string name="export_format_google_auth_uri">Text file (.TXT)</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_format_hint">Export format</string>
|
||||||
<string name="export_all_groups">Export all groups</string>
|
<string name="export_all_groups">Export all groups</string>
|
||||||
<string name="export_choose_groups">Select which groups to export:</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_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="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>
|
<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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue