Load vault file on demand instead of juggling it around in-memory

This trades performance for making VaultManager a bit easier to reason
about.

This also fixes a rare crash that could occur if the user retries to unlock
the app after the previous attempt resulted in an error related to
parsing the vault. The vault file would no longer be present in memory
after the first attempt, causing the second attempt to crash the app.
This commit is contained in:
Alexander Bakker 2023-03-01 18:01:25 +01:00
parent f7bac4331e
commit 32e462bdce
5 changed files with 53 additions and 93 deletions

View File

@ -1,7 +1,6 @@
package com.beemdevelopment.aegis;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@ -32,11 +31,10 @@ public class PanicTriggerTest extends AegisTest {
@Test
public void testPanicTriggerDisabled() {
assertFalse(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertTrue(_vaultManager.isVaultLoaded());
_vaultManager.getVault();
assertFalse(_vaultManager.isVaultFileLoaded());
assertNull(_vaultManager.getVaultFileError());
assertTrue(VaultRepository.fileExists(getApp()));
}
@ -44,11 +42,10 @@ public class PanicTriggerTest extends AegisTest {
public void testPanicTriggerEnabled() {
_prefs.setIsPanicTriggerEnabled(true);
assertTrue(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertFalse(_vaultManager.isVaultLoaded());
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
assertFalse(_vaultManager.isVaultFileLoaded());
assertNull(_vaultManager.getVaultFileError());
assertFalse(VaultRepository.fileExists(getApp()));
}

View File

@ -36,6 +36,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@ -55,7 +56,9 @@ public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private VaultFile _vaultFile;
private SlotList _slots;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
@ -104,17 +107,17 @@ public class AuthActivity extends AegisActivity {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
if (_vaultManager.getVaultFileError() != null) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog, which) -> {
try {
_vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
getOnBackPressedDispatcher().onBackPressed();
});
return;
}
VaultFile vaultFile = _vaultManager.getVaultFile();
_slots = vaultFile.getHeader().getSlots();
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
_slots = _vaultFile.getHeader().getSlots();
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
boolean invalidated = false;
@ -287,7 +290,7 @@ public class AuthActivity extends AegisActivity {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
try {
_vaultManager.unlock(creds);
_vaultManager.loadFrom(_vaultFile, creds);
if (isSlotRepaired) {
saveAndBackupVault();
}

View File

@ -21,7 +21,9 @@ import com.beemdevelopment.aegis.ui.slides.DoneSlide;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@ -108,8 +110,17 @@ public class IntroActivity extends IntroBaseActivity {
return;
}
} else {
VaultFile vaultFile;
try {
_vaultManager.load(creds);
vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
return;
}
try {
_vaultManager.loadFrom(vaultFile, creds);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);

View File

@ -55,6 +55,9 @@ import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.common.base.Strings;
@ -663,9 +666,30 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
if (!_vaultManager.isVaultLoaded() && !_vaultManager.isVaultFileLoaded()) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog1, which) -> finish());
return;
// If the vault is not loaded yet, try to load it now in case it's plain text
if (!_vaultManager.isVaultLoaded()) {
VaultFile vaultFile;
try {
vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
finish();
});
return;
}
if (!vaultFile.isEncrypted()) {
try {
_vaultManager.loadFrom(vaultFile);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
finish();
});
return;
}
}
}
if (!_vaultManager.isVaultLoaded()) {

View File

@ -19,7 +19,6 @@ import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@ -30,8 +29,6 @@ public class VaultManager {
private final Context _context;
private final Preferences _prefs;
private VaultFile _vaultFile;
private VaultRepositoryException _vaultFileError;
private VaultRepository _repo;
private final VaultBackupManager _backups;
@ -46,35 +43,11 @@ public class VaultManager {
_backups = new VaultBackupManager(_context);
_androidBackups = new BackupManager(context);
_lockListeners = new ArrayList<>();
loadVaultFile();
}
private void loadVaultFile() {
try {
_vaultFile = VaultRepository.readVaultFile(_context);
} catch (VaultRepositoryException e) {
if (!(e.getCause() instanceof FileNotFoundException)) {
_vaultFileError = e;
e.printStackTrace();
}
}
if (_vaultFile != null && !_vaultFile.isEncrypted()) {
try {
loadFrom(_vaultFile, null);
} catch (VaultRepositoryException e) {
e.printStackTrace();
_vaultFile = null;
_vaultFileError = e;
}
}
}
/**
* Initializes the vault repository with a new empty vault and 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 initNew(@Nullable VaultFileCredentials creds) throws VaultRepositoryException {
@ -82,8 +55,6 @@ public class VaultManager {
throw new IllegalStateException("Vault manager is already initialized");
}
_vaultFile = null;
_vaultFileError = null;
_repo = new VaultRepository(_context, new Vault(), creds);
save();
@ -97,8 +68,6 @@ public class VaultManager {
/**
* Initializes the vault repository by decrypting the given vaultFile 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 loadFrom(@NonNull VaultFile vaultFile, @Nullable VaultFileCredentials creds) throws VaultRepositoryException {
@ -106,8 +75,6 @@ public class VaultManager {
throw new IllegalStateException("Vault manager is already initialized");
}
_vaultFile = null;
_vaultFileError = null;
_repo = VaultRepository.fromFile(_context, vaultFile, creds);
if (getVault().isEncryptionEnabled()) {
@ -117,32 +84,9 @@ 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 = loadFrom(getVaultFile(), creds);
startNotificationService();
return repo;
public VaultRepository loadFrom(@NonNull VaultFile vaultFile) throws VaultRepositoryException {
return loadFrom(vaultFile, null);
}
/**
@ -157,7 +101,6 @@ public class VaultManager {
}
stopNotificationService();
loadVaultFile();
}
public void enableEncryption(VaultFileCredentials creds) throws VaultRepositoryException {
@ -270,12 +213,8 @@ public class VaultManager {
return _repo != null;
}
public boolean isVaultFileLoaded() {
return _vaultFile != null;
}
public boolean isVaultInitNeeded() {
return !isVaultLoaded() && !isVaultFileLoaded() && getVaultFileError() == null;
return !isVaultLoaded() && !VaultRepository.fileExists(_context);
}
@NonNull
@ -287,20 +226,6 @@ public class VaultManager {
return _repo;
}
@NonNull
public VaultFile getVaultFile() {
if (_vaultFile == null) {
throw new IllegalStateException("Vault file is not in memory");
}
return _vaultFile;
}
@Nullable
public VaultRepositoryException getVaultFileError() {
return _vaultFileError;
}
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.