From 79022be3b613b17f950e3921df3e42e2b5849580 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sat, 4 Jun 2022 18:34:52 +0200 Subject: [PATCH] Add an import button to the intro --- .../com/beemdevelopment/aegis/AegisTest.java | 2 +- .../com/beemdevelopment/aegis/IntroTest.java | 70 ++++++++++++++ .../aegis/aegis_encrypted.json | 1 + .../beemdevelopment/aegis/aegis_plain.json | 1 + .../aegis/importers/AegisImporter.java | 14 ++- .../aegis/ui/IntroActivity.java | 49 +++++++--- .../aegis/ui/intro/IntroBaseActivity.java | 14 ++- .../aegis/ui/slides/WelcomeSlide.java | 95 ++++++++++++++++++- .../aegis/vault/VaultManager.java | 29 +++++- .../res/layout/fragment_welcome_slide.xml | 14 +++ app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 9 ++ 13 files changed, 277 insertions(+), 24 deletions(-) create mode 120000 app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_encrypted.json create mode 120000 app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_plain.json diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java index 8d52459f..d3dfff30 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java @@ -74,7 +74,7 @@ public abstract class AegisTest { private VaultRepository initVault(@Nullable VaultFileCredentials creds, @Nullable List entries) { VaultRepository vault; try { - vault = _vaultManager.init(creds); + vault = _vaultManager.initNew(creds); } catch (VaultRepositoryException e) { throw new RuntimeException(e); } diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java index dd490a18..7fe70971 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java @@ -6,16 +6,25 @@ import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.intent.Intents.intending; +import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.not; +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Intent; +import android.net.Uri; + import androidx.test.espresso.IdlingRegistry; import androidx.test.espresso.IdlingResource; import androidx.test.espresso.ViewInteraction; +import androidx.test.espresso.intent.Intents; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; @@ -23,6 +32,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.beemdevelopment.aegis.rules.ScreenshotTestRule; import com.beemdevelopment.aegis.ui.IntroActivity; +import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.slots.BiometricSlot; import com.beemdevelopment.aegis.vault.slots.PasswordSlot; @@ -36,6 +46,11 @@ import org.junit.rules.RuleChain; import org.junit.rules.TestRule; import org.junit.runner.RunWith; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + import dagger.hilt.android.testing.HiltAndroidTest; @RunWith(AndroidJUnit4.class) @@ -51,6 +66,8 @@ public class IntroTest extends AegisTest { @Before public void setUp() { + Intents.init(); + _activityRule.getScenario().onActivity(activity -> { _viewPager2IdlingResource = new ViewPager2IdlingResource(activity.findViewById(R.id.pager), "viewPagerIdlingResource"); IdlingRegistry.getInstance().register(_viewPager2IdlingResource); @@ -59,6 +76,7 @@ public class IntroTest extends AegisTest { @After public void tearDown() { + Intents.release(); IdlingRegistry.getInstance().unregister(_viewPager2IdlingResource); } @@ -113,6 +131,58 @@ public class IntroTest extends AegisTest { assertFalse(slots.has(BiometricSlot.class)); } + @Test + public void doIntro_Import_Plain() { + Uri uri = getResourceUri("aegis_plain.json"); + Intent resultData = new Intent(); + resultData.setData(uri); + + Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData); + intending(not(isInternal())).respondWith(result); + + ViewInteraction next = onView(withId(R.id.btnNext)); + onView(withId(R.id.btnImport)).perform(click()); + next.perform(click()); + + VaultRepository vault = _vaultManager.getVault(); + assertFalse(vault.isEncryptionEnabled()); + assertNull(vault.getCredentials()); + } + + @Test + public void doIntro_Import_Encrypted() { + Uri uri = getResourceUri("aegis_encrypted.json"); + Intent resultData = new Intent(); + resultData.setData(uri); + + Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData); + intending(not(isInternal())).respondWith(result); + + ViewInteraction next = onView(withId(R.id.btnNext)); + onView(withId(R.id.btnImport)).perform(click()); + onView(withId(R.id.text_input)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard()); + onView(withId(android.R.id.button1)).perform(click()); + next.perform(click()); + + VaultRepository vault = _vaultManager.getVault(); + SlotList slots = vault.getCredentials().getSlots(); + assertTrue(vault.isEncryptionEnabled()); + assertTrue(slots.has(PasswordSlot.class)); + assertFalse(slots.has(BiometricSlot.class)); + } + + private Uri getResourceUri(String resourceName) { + File targetFile = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), resourceName); + try (InputStream inStream = getClass().getResourceAsStream(resourceName); + FileOutputStream outStream = new FileOutputStream(targetFile)) { + IOUtils.copy(inStream, outStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return Uri.fromFile(targetFile); + } + // Source: https://stackoverflow.com/a/32763454/12972657 private static class ViewPager2IdlingResource implements IdlingResource { private final String _resName; diff --git a/app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_encrypted.json b/app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_encrypted.json new file mode 120000 index 00000000..02a115f7 --- /dev/null +++ b/app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_encrypted.json @@ -0,0 +1 @@ +../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_encrypted.json \ No newline at end of file diff --git a/app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_plain.json b/app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_plain.json new file mode 120000 index 00000000..b710d05f --- /dev/null +++ b/app/src/androidTest/resources/com/beemdevelopment/aegis/aegis_plain.json @@ -0,0 +1 @@ +../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_plain.json \ No newline at end of file diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java index 75d86473..c01b13df 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java @@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; import android.content.DialogInterface; +import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import com.beemdevelopment.aegis.R; @@ -72,7 +73,7 @@ public class AegisImporter extends DatabaseImporter { throw new DatabaseImporterException(e); } - return new DecryptedState(obj); + return new DecryptedState(obj, creds); } public State decrypt(char[] password) throws DatabaseImporterException { @@ -109,10 +110,21 @@ public class AegisImporter extends DatabaseImporter { public static class DecryptedState extends State { private JSONObject _obj; + private VaultFileCredentials _creds; private DecryptedState(JSONObject obj) { + this(obj, null); + } + + private DecryptedState(JSONObject obj, VaultFileCredentials creds) { super(false); _obj = obj; + _creds = creds; + } + + @Nullable + public VaultFileCredentials getCredentials() { + return _creds; } @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java index 12e1ca43..ae2eb99d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java @@ -6,6 +6,7 @@ import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS; import android.os.Bundle; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import com.beemdevelopment.aegis.R; @@ -51,6 +52,18 @@ public class IntroActivity extends IntroBaseActivity { return true; } + if (oldSlide == WelcomeSlide.class + && newSlide == SecurityPickerSlide.class + && getState().getBoolean("imported")) { + skipToSlide(DoneSlide.class); + return true; + } + + // on the welcome page, we don't want the keyboard to push any views up + getWindow().setSoftInputMode(newSlide == WelcomeSlide.class + ? WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING + : WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + return false; } @@ -58,21 +71,31 @@ public class IntroActivity extends IntroBaseActivity { protected void onDonePressed() { Bundle state = getState(); - int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID); VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds"); - if (cryptType == CRYPT_TYPE_INVALID - || (cryptType == CRYPT_TYPE_NONE && creds != null) - || (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class))) - || (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) { - throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds)); - } + if (!state.getBoolean("imported")) { + int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID); + if (cryptType == CRYPT_TYPE_INVALID + || (cryptType == CRYPT_TYPE_NONE && creds != null) + || (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class))) + || (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) { + throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds)); + } - try { - _vaultManager.init(creds); - } catch (VaultRepositoryException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(this, R.string.vault_init_error, e); - return; + try { + _vaultManager.initNew(creds); + } catch (VaultRepositoryException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(this, R.string.vault_init_error, e); + return; + } + } else { + try { + _vaultManager.load(creds); + } catch (VaultRepositoryException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(this, R.string.vault_load_error, e); + return; + } } // skip the intro from now on diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java index aecf53a5..09d3ee85 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java @@ -1,11 +1,11 @@ package com.beemdevelopment.aegis.ui.intro; -import android.content.res.Configuration; import android.os.Bundle; import android.view.View; import android.widget.ImageButton; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; @@ -13,7 +13,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.beemdevelopment.aegis.R; -import com.beemdevelopment.aegis.Theme; import com.beemdevelopment.aegis.ui.AegisActivity; import java.lang.ref.WeakReference; @@ -116,7 +115,7 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc * @param newSlide the next slide that will be shown. * @return whether to block the transition. */ - protected boolean onBeforeSlideChanged(Class oldSlide, Class newSlide) { + protected boolean onBeforeSlideChanged(@Nullable Class oldSlide, @NonNull Class newSlide) { return false; } @@ -125,7 +124,7 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc * @param oldSlide the slide that was previously shown. * @param newSlide the slide that is now shown. */ - protected void onAfterSlideChanged(Class oldSlide, Class newSlide) { + protected void onAfterSlideChanged(@Nullable Class oldSlide, @NonNull Class newSlide) { } @@ -178,6 +177,13 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc _slides.add(type); _slideIndicator.setSlideCount(_slides.size()); + + // send 'slide changed' events for the first slide + if (_slides.size() == 1) { + Class slide = _slides.get(0); + onBeforeSlideChanged(null, slide); + onAfterSlideChanged(null, slide); + } } private class ScreenSlidePagerAdapter extends FragmentStateAdapter { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java index ada16d85..a3428822 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java @@ -1,16 +1,109 @@ package com.beemdevelopment.aegis.ui.slides; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; + import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.importers.AegisImporter; +import com.beemdevelopment.aegis.importers.DatabaseImporter; +import com.beemdevelopment.aegis.importers.DatabaseImporterException; +import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.intro.SlideFragment; +import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; +import com.beemdevelopment.aegis.vault.VaultFileCredentials; +import com.beemdevelopment.aegis.vault.VaultRepository; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; public class WelcomeSlide extends SlideFragment { + public static final int CODE_IMPORT_VAULT = 0; + + private boolean _imported; + private VaultFileCredentials _creds; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_welcome_slide, container, false); + View view = inflater.inflate(R.layout.fragment_welcome_slide, container, false); + view.findViewById(R.id.btnImport).setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + startActivityForResult(intent, CODE_IMPORT_VAULT); + }); + return view; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == CODE_IMPORT_VAULT && data != null && data.getData() != null) { + startImportVault(data.getData()); + } + } + + @Override + public void onSaveIntroState(@NonNull Bundle introState) { + introState.putBoolean("imported", _imported); + introState.putSerializable("creds", _creds); + } + + private void startImportVault(Uri uri) { + ImportFileTask.Params params = new ImportFileTask.Params(uri, "intro-import", null); + ImportFileTask task = new ImportFileTask(requireContext(), result -> { + if (result.getException() != null) { + Dialogs.showErrorDialog(requireContext(), R.string.reading_file_error, result.getException()); + return; + } + + try (FileInputStream inStream = new FileInputStream(result.getFile())) { + AegisImporter importer = new AegisImporter(requireContext()); + DatabaseImporter.State state = importer.read(inStream, false); + if (state.isEncrypted()) { + state.decrypt(requireContext(), new DatabaseImporter.DecryptListener() { + @Override + protected void onStateDecrypted(DatabaseImporter.State state) { + _creds = ((AegisImporter.DecryptedState) state).getCredentials(); + importVault(result.getFile()); + } + + @Override + protected void onError(Exception e) { + e.printStackTrace(); + Dialogs.showErrorDialog(requireContext(), R.string.decryption_error, e); + } + + @Override + protected void onCanceled() { + + } + }); + } else { + importVault(result.getFile()); + } + } catch (DatabaseImporterException | IOException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(requireContext(), R.string.intro_import_error_title, e); + } + }); + task.execute(getLifecycle(), params); + } + + private void importVault(File file) { + try (FileInputStream inStream = new FileInputStream(file)) { + VaultRepository.writeToFile(requireContext(), inStream); + } catch (IOException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(requireContext(), R.string.intro_import_error_title, e); + return; + } + + _imported = true; + goToNextSlide(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java index 206c685d..df414587 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java @@ -60,7 +60,7 @@ public class VaultManager { if (_vaultFile != null && !_vaultFile.isEncrypted()) { try { - load(_vaultFile, null); + loadFrom(_vaultFile, null); } catch (VaultRepositoryException e) { e.printStackTrace(); _vaultFile = null; @@ -76,7 +76,7 @@ public class VaultManager { * Calling this method removes the manager's internal reference to the raw vault file (if it had one). */ @NonNull - public VaultRepository init(@Nullable VaultFileCredentials creds) throws VaultRepositoryException { + public VaultRepository initNew(@Nullable VaultFileCredentials creds) throws VaultRepositoryException { if (isVaultLoaded()) { throw new IllegalStateException("Vault manager is already initialized"); } @@ -100,7 +100,7 @@ public class VaultManager { * Calling this method removes the manager's internal reference to the raw vault file (if it had one). */ @NonNull - public VaultRepository load(@NonNull VaultFile vaultFile, @Nullable VaultFileCredentials creds) throws VaultRepositoryException { + public VaultRepository loadFrom(@NonNull VaultFile vaultFile, @Nullable VaultFileCredentials creds) throws VaultRepositoryException { if (isVaultLoaded()) { throw new IllegalStateException("Vault manager is already initialized"); } @@ -116,9 +116,30 @@ public class VaultManager { return getVault(); } + /** + * Initializes the vault repository by loading and decrypting the vault file stored in + * internal storage, with the given creds. It can only be called if isVaultLoaded() + * returns false. + * + * Calling this method removes the manager's internal reference to the raw vault file (if it had one). + */ + @NonNull + public VaultRepository load(@Nullable VaultFileCredentials creds) throws VaultRepositoryException { + if (isVaultLoaded()) { + throw new IllegalStateException("Vault manager is already initialized"); + } + + loadVaultFile(); + if (isVaultLoaded()) { + return _repo; + } + + return loadFrom(getVaultFile(), creds); + } + @NonNull public VaultRepository unlock(@NonNull VaultFileCredentials creds) throws VaultRepositoryException { - VaultRepository repo = load(getVaultFile(), creds); + VaultRepository repo = loadFrom(getVaultFile(), creds); startNotificationService(); return repo; } diff --git a/app/src/main/res/layout/fragment_welcome_slide.xml b/app/src/main/res/layout/fragment_welcome_slide.xml index a6c80eeb..8cf5cbe9 100644 --- a/app/src/main/res/layout/fragment_welcome_slide.xml +++ b/app/src/main/res/layout/fragment_welcome_slide.xml @@ -28,13 +28,27 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/titleText"/> +