Add instrumented tests for backup and export

pull/926/head
Alexander Bakker 3 years ago
parent a492bcbde3
commit 5db365a999

@ -36,7 +36,6 @@ android {
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'

@ -2,6 +2,7 @@ package com.beemdevelopment.aegis;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.platform.app.InstrumentationRegistry;
@ -16,6 +17,7 @@ import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.hamcrest.Matcher;
import org.junit.Before;
@ -25,6 +27,7 @@ import java.lang.reflect.InvocationTargetException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
@ -34,6 +37,7 @@ import dagger.hilt.android.testing.HiltAndroidRule;
public abstract class AegisTest {
public static final String VAULT_PASSWORD = "test";
public static final String VAULT_BACKUP_PASSWORD = "something";
@Rule
public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
@ -53,8 +57,21 @@ public abstract class AegisTest {
return (AegisApplicationBase) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
protected VaultRepository initVault() {
protected VaultRepository initEncryptedVault() {
VaultFileCredentials creds = generateCredentials();
return initVault(creds, VaultEntries.get());
}
protected VaultRepository initEmptyEncryptedVault() {
VaultFileCredentials creds = generateCredentials();
return initVault(creds, null);
}
protected VaultRepository initPlainVault() {
return initVault(null, VaultEntries.get());
}
private VaultRepository initVault(@Nullable VaultFileCredentials creds, @Nullable List<VaultEntry> entries) {
VaultRepository vault;
try {
vault = _vaultManager.init(creds);
@ -62,6 +79,18 @@ public abstract class AegisTest {
throw new RuntimeException(e);
}
if (entries != null) {
for (VaultEntry entry : entries) {
_vaultManager.getVault().addEntry(entry);
}
}
try {
_vaultManager.save();
} catch (VaultRepositoryException e) {
throw new RuntimeException(e);
}
_prefs.setIntroDone(true);
return vault;
}

@ -0,0 +1,302 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.importers.GoogleAuthUriImporter;
import com.beemdevelopment.aegis.ui.PreferencesActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class BackupExportTest extends AegisTest {
@Rule
public final ActivityScenarioRule<PreferencesActivity> activityRule = new ActivityScenarioRule<>(PreferencesActivity.class);
@Before
public void setUp() {
Intents.init();
}
@After
public void tearDown() {
Intents.release();
}
@Test
public void testPlainVaultExportPlainJson() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readVault(file, null);
}
@Test
public void testPlainVaultExportPlainTxt() {
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_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readTxtExport(file);
}
@Test
public void testPlainVaultExportEncryptedJson() {
initPlainVault();
openExportDialog();
File file = doExport();
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
readVault(file, VAULT_PASSWORD);
}
@Test
public void testEncryptedVaultExportPlainJson() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readVault(file, null);
}
@Test
public void testEncryptedVaultExportPlainTxt() {
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_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readTxtExport(file);
}
@Test
public void testEncryptedVaultExportEncryptedJson() {
initEncryptedVault();
openExportDialog();
File file = doExport();
readVault(file, VAULT_PASSWORD);
}
@Test
public void testSeparateExportPassword() {
initEncryptedVault();
setSeparateBackupExportPassword();
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD);
}
private void setSeparateBackupExportPassword() {
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 0);
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
assertThrows(SlotIntegrityException.class, () -> decryptPasswordSlot(slot, VAULT_PASSWORD));
MasterKey masterKey;
try {
masterKey = decryptPasswordSlot(slot, VAULT_BACKUP_PASSWORD);
} catch (SlotIntegrityException e) {
throw new RuntimeException("Unable to decrypt password slot", e);
}
assertArrayEquals(creds.getKey().getBytes(), masterKey.getBytes());
}
}
private File doExport() {
File file = getExportFileUri();
Intent resultData = new Intent();
resultData.setData(Uri.fromFile(file));
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
onView(withId(android.R.id.button1)).perform(click());
return file;
}
private void openExportDialog() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_import_export_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_export_title)), click()));
}
private MasterKey decryptPasswordSlot(PasswordSlot slot, String password) throws SlotIntegrityException {
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
try {
Cipher cipher = slot.createDecryptCipher(derivedKey);
return slot.getKey(cipher);
} catch (SlotException e) {
throw new RuntimeException("Unable to decrypt password slot", e);
}
}
private File getExportFileUri() {
String dirName = Hex.encode(CryptoUtils.generateRandomBytes(8));
File dir = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), String.format("export-%s", dirName));
if (!dir.mkdirs()) {
throw new RuntimeException(String.format("Unable to create export directory: %s", dir));
}
VaultBackupManager.FileInfo fileInfo = new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT);
return new File(dir, fileInfo.toString());
}
private VaultRepository readVault(File file, @Nullable String password) {
VaultRepository repo;
try (InputStream inStream = new FileInputStream(file)) {
byte[] bytes = IOUtils.readAll(inStream);
VaultFile vaultFile = VaultFile.fromBytes(bytes);
VaultFileCredentials creds = null;
if (password != null) {
SlotList slots = vaultFile.getHeader().getSlots();
for (PasswordSlot slot : slots.findAll(PasswordSlot.class)) {
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
Cipher cipher = slot.createDecryptCipher(derivedKey);
MasterKey masterKey = slot.getKey(cipher);
creds = new VaultFileCredentials(masterKey, slots);
break;
}
}
repo = VaultRepository.fromFile(getInstrumentation().getContext(), vaultFile, creds);
} catch (SlotException | SlotIntegrityException | VaultRepositoryException | VaultFileException | IOException e) {
throw new RuntimeException("Unable to read back vault file", e);
}
checkReadEntries(repo.getEntries());
return repo;
}
private void readTxtExport(File file) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getInstrumentation().getContext());
Collection<VaultEntry> entries;
try (InputStream inStream = new FileInputStream(file)) {
DatabaseImporter.State state = importer.read(inStream);
DatabaseImporter.Result result = state.convert();
entries = result.getEntries().getValues();
} catch (DatabaseImporterException | IOException e) {
throw new RuntimeException("Unable to read txt export file", e);
}
checkReadEntries(entries);
}
private void checkReadEntries(Collection<VaultEntry> entries) {
List<VaultEntry> vectors = VaultEntries.get();
assertEquals(vectors.size(), entries.size());
int i = 0;
for (VaultEntry entry : entries) {
VaultEntry vector = vectors.get(i);
String message = String.format("Entries are not equivalent: (%s) (%s)", vector.toJson().toString(), entry.toJson().toString());
assertTrue(message, vector.equivalates(entry));
assertEquals(message, vector.getInfo().getOtp(), entry.getInfo().getOtp());
i++;
}
}
}

@ -29,7 +29,7 @@ import dagger.hilt.android.testing.HiltAndroidTest;
public class DeepLinkTest extends AegisTest {
@Before
public void before() {
initVault();
initEmptyEncryptedVault();
}
@Test

@ -26,7 +26,7 @@ import dagger.hilt.android.testing.HiltAndroidTest;
public class PanicTriggerTest extends AegisTest {
@Before
public void before() {
initVault();
initEncryptedVault();
}
@Test

@ -24,7 +24,7 @@ import dagger.hilt.android.testing.HiltAndroidTest;
public class VaultRepositoryTest extends AegisTest {
@Before
public void before() {
initVault();
initEncryptedVault();
}
@Test

@ -0,0 +1 @@
../../../../../../test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java

@ -34,7 +34,6 @@ import com.beemdevelopment.aegis.vault.slots.SlotList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
@ -165,7 +164,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
Dialogs.showPasswordInputDialog(requireContext(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> {
if (isDigitsOnly(new String(password))) {
List<PasswordSlot> slots = getRegularPasswordSlots(_vaultManager.getVault().getCredentials().getSlots());
List<PasswordSlot> slots = _vaultManager.getVault().getCredentials().getSlots().findRegularPasswordSlots();
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(requireContext(), new PasswordConfirmationListener());
task.execute(getLifecycle(), params);
@ -243,7 +242,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
Dialogs.showSetPasswordDialog(requireActivity(), new SetBackupPasswordListener());
} else {
SlotList slots = _vaultManager.getVault().getCredentials().getSlots();
for (Slot slot : getBackupPasswordSlots(slots)) {
for (Slot slot : slots.findBackupPasswordSlots()) {
slots.remove(slot);
}
@ -275,8 +274,8 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
if (encrypted) {
SlotList slots = _vaultManager.getVault().getCredentials().getSlots();
boolean multiBackupPassword = getBackupPasswordSlots(slots).size() > 1;
boolean multiPassword = getRegularPasswordSlots(slots).size() > 1;
boolean multiBackupPassword = slots.findBackupPasswordSlots().size() > 1;
boolean multiPassword = slots.findRegularPasswordSlots().size() > 1;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean canUseBio = BiometricsHelper.isAvailable(requireContext());
_setPasswordPreference.setEnabled(!multiPassword);
@ -299,23 +298,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
return false;
}
return getBackupPasswordSlots(vault.getCredentials().getSlots()).size() > 0;
}
private static List<PasswordSlot> getBackupPasswordSlots(SlotList slots) {
return getPasswordSlots(slots, true);
}
private static List<PasswordSlot> getRegularPasswordSlots(SlotList slots) {
return getPasswordSlots(slots, false);
}
private static List<PasswordSlot> getPasswordSlots(SlotList slots, boolean isBackup) {
return slots
.findAll(PasswordSlot.class)
.stream()
.filter(s -> s.isBackup() == isBackup)
.collect(Collectors.toList());
return vault.getCredentials().getSlots().findBackupPasswordSlots().size() > 0;
}
private String getPasswordReminderSummary() {
@ -403,7 +386,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
slot.setKey(creds.getKey(), cipher);
// remove the old backup password slot
for (Slot oldSlot : getBackupPasswordSlots(slots)) {
for (Slot oldSlot : slots.findBackupPasswordSlots()) {
slots.remove(oldSlot);
}

@ -8,6 +8,7 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SlotList extends UUIDMap<Slot> {
public JSONArray toJson() {
@ -54,6 +55,20 @@ public class SlotList extends UUIDMap<Slot> {
return list;
}
public List<PasswordSlot> findBackupPasswordSlots() {
return findAll(PasswordSlot.class)
.stream()
.filter(PasswordSlot::isBackup)
.collect(Collectors.toList());
}
public List<PasswordSlot> findRegularPasswordSlots() {
return findAll(PasswordSlot.class)
.stream()
.filter(s -> !s.isBackup())
.collect(Collectors.toList());
}
public <T extends Slot> boolean has(Class<T> type) {
return find(type) != null;
}

@ -11,8 +11,8 @@ import android.os.Build;
import androidx.test.core.app.ApplicationProvider;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@ -48,16 +48,8 @@ public class DatabaseImporterTest {
*/
@Before
public void initVectors() throws EncodingException, OtpInfoException {
_vectors = Lists.newArrayList(
new VaultEntry(new TotpInfo(Base32.decode("4SJHB4GSD43FZBAI7C2HLRJGPQ")), "Mason", "Deno"),
new VaultEntry(new TotpInfo(Base32.decode("5OM4WOOGPLQEF6UGN3CPEOOLWU"), "SHA256", 7, 20), "James", "SPDX"),
new VaultEntry(new TotpInfo(Base32.decode("7ELGJSGXNCCTV3O6LKJWYFV2RA"), "SHA512", 8, 50), "Elijah", "Airbnb"),
new VaultEntry(new HotpInfo(Base32.decode("YOOMIXWS5GN6RTBPUFFWKTW5M4"), "SHA1", 6, 1), "James", "Issuu"),
new VaultEntry(new HotpInfo(Base32.decode("KUVJJOM753IHTNDSZVCNKL7GII"), "SHA256", 7, 50), "Benjamin", "Air Canada"),
new VaultEntry(new HotpInfo(Base32.decode("5VAML3X35THCEBVRLV24CGBKOY"), "SHA512", 8, 10300), "Mason", "WWE"),
new VaultEntry(new SteamInfo(Base32.decode("JRZCL47CMXVOQMNPZR2F7J4RGI"), "SHA1", 5, 30), "Sophia", "Boeing")
);
public void initVectors() {
_vectors = VaultEntries.get();
}
@Test

@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.vectors;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.collect.Lists;
import java.util.List;
public class VaultEntries {
private VaultEntries() {
}
public static List<VaultEntry> get() {
try {
return Lists.newArrayList(
new VaultEntry(new TotpInfo(Base32.decode("4SJHB4GSD43FZBAI7C2HLRJGPQ")), "Mason", "Deno"),
new VaultEntry(new TotpInfo(Base32.decode("5OM4WOOGPLQEF6UGN3CPEOOLWU"), "SHA256", 7, 20), "James", "SPDX"),
new VaultEntry(new TotpInfo(Base32.decode("7ELGJSGXNCCTV3O6LKJWYFV2RA"), "SHA512", 8, 50), "Elijah", "Airbnb"),
new VaultEntry(new HotpInfo(Base32.decode("YOOMIXWS5GN6RTBPUFFWKTW5M4"), "SHA1", 6, 1), "James", "Issuu"),
new VaultEntry(new HotpInfo(Base32.decode("KUVJJOM753IHTNDSZVCNKL7GII"), "SHA256", 7, 50), "Benjamin", "Air Canada"),
new VaultEntry(new HotpInfo(Base32.decode("5VAML3X35THCEBVRLV24CGBKOY"), "SHA512", 8, 10300), "Mason", "WWE"),
new VaultEntry(new SteamInfo(Base32.decode("JRZCL47CMXVOQMNPZR2F7J4RGI"), "SHA1", 5, 30), "Sophia", "Boeing")
);
} catch (OtpInfoException | EncodingException e) {
throw new RuntimeException(e);
}
}
}
Loading…
Cancel
Save